PathIterator.kt

/*
 * Copyright 2022 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.
 */
@file:JvmName("PathUtilities")
package androidx.graphics.path

import android.graphics.Path
import androidx.core.os.BuildCompat
import androidx.core.os.BuildCompat.PrereleaseSdkCheck

/**
 * A path iterator can be used to iterate over all the [segments][PathSegment] that make up
 * a path. Those segments may in turn define multiple contours inside the path. Conic segments
 * are by default evaluated as approximated quadratic segments. To preserve conic segments as
 * conics, set [conicEvaluation] to [AsConic][ConicEvaluation.AsConic]. The error of the
 * approximation is controlled by [tolerance].
 *
 * [PathIterator] objects are created implicitly through a given [Path] object; to create a
 * [PathIterator], call one of the two [Path.iterator] extension functions.
 */
@Suppress("NotCloseable", "IllegalExperimentalApiUsage")
@PrereleaseSdkCheck
class PathIterator constructor(
    val path: Path,
    val conicEvaluation: ConicEvaluation = ConicEvaluation.AsQuadratics,
    val tolerance: Float = 0.25f
) : Iterator<PathSegment> {

    internal val implementation: PathIteratorImpl
    init {
        implementation =
            when {
                // TODO: replace isAtLeastU() check with below or similar when U is released
                // Build.VERSION.SDK_INT >= 34 -> {
                BuildCompat.isAtLeastU() -> {
                    PathIteratorApi34Impl(path, conicEvaluation, tolerance)
                }
                else -> {
                    PathIteratorPreApi34Impl(path, conicEvaluation, tolerance)
                }
            }
    }

    enum class ConicEvaluation {
        /**
         * Conic segments are returned as conic segments.
         */
        AsConic,

        /**
         * Conic segments are returned as quadratic approximations. The quality of the
         * approximation is defined by a tolerance value.
         */
        AsQuadratics
    }

    /**
     * Returns the number of verbs present in this iterator, i.e. the number of calls to
     * [next] required to complete the iteration.
     *
     * By default, [calculateSize] returns the true number of operations in the iterator. Deriving
     * this result requires converting any conics to quadratics, if [conicEvaluation] is
     * set to [ConicEvaluation.AsQuadratics], which takes extra processing time. Set
     * [includeConvertedConics] to false if an approximate size, not including conic
     * conversion, is sufficient.
     *
     * @param includeConvertedConics The returned size includes any required conic conversions.
     * Default is true, so it will return the exact size, at the cost of iterating through
     * all elements and converting any conics as appropriate. Set to false to save on processing,
     * at the cost of a less exact result.
     */
    fun calculateSize(includeConvertedConics: Boolean = true) =
        implementation.calculateSize(includeConvertedConics)

    /**
     * Returns `true` if the iteration has more elements.
     */
    override fun hasNext(): Boolean = implementation.hasNext()

    /**
     * Returns the type of the current segment in the iteration, or [Done][PathSegment.Type.Done]
     * if the iteration is finished.
     */
    fun peek() = implementation.peek()

    /**
     * Returns the [type][PathSegment.Type] of the next [path segment][PathSegment] in the iteration
     * and fills [points] with the points specific to the segment type. Each pair of floats in
     * the [points] array represents a point for the given segment. The number of pairs of floats
     * depends on the [PathSegment.Type]:
     * - [Move][PathSegment.Type.Move]: 1 pair (indices 0 to 1)
     * - [Move][PathSegment.Type.Line]: 2 pairs (indices 0 to 3)
     * - [Move][PathSegment.Type.Quadratic]: 3 pairs (indices 0 to 5)
     * - [Move][PathSegment.Type.Conic]: 4 pairs (indices 0 to 7), the last pair contains the
     *   [weight][PathSegment.weight] twice
     * - [Move][PathSegment.Type.Cubic]: 4 pairs (indices 0 to 7)
     * - [Close][PathSegment.Type.Close]: 0 pair
     * - [Done][PathSegment.Type.Done]: 0 pair
     * This method does not allocate any memory.
     *
     * @param points A [FloatArray] large enough to hold 8 floats starting at [offset],
     *               throws an [IllegalStateException] otherwise.
     * @param offset Offset in [points] where to store the result
     */
    @JvmOverloads
    fun next(points: FloatArray, offset: Int = 0): PathSegment.Type =
        implementation.next(points, offset)

    /**
     * Returns the next [path segment][PathSegment] in the iteration, or [DoneSegment] if
     * the iteration is finished. To save on allocations, use the alternative [next] function, which
     * takes a [FloatArray].
     */
    override fun next(): PathSegment = implementation.next()
}

/**
 * Creates a new [PathIterator] for this [path][android.graphics.Path] that evaluates
 * conics as quadratics. To preserve conics, use the [Path.iterator] function that takes a
 * [PathIterator.ConicEvaluation] parameter.
 */
@Suppress("IllegalExperimentalApiUsage")
@PrereleaseSdkCheck
operator fun Path.iterator() = PathIterator(this)

/**
 * Creates a new [PathIterator] for this [path][android.graphics.Path]. To preserve conics as
 * conics (not convert them to quadratics), set [conicEvaluation] to
 * [PathIterator.ConicEvaluation.AsConic].
 */
@Suppress("IllegalExperimentalApiUsage")
@PrereleaseSdkCheck
fun Path.iterator(conicEvaluation: PathIterator.ConicEvaluation, tolerance: Float = 0.25f) =
    PathIterator(this, conicEvaluation, tolerance)