All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.adjust.ContourEdge.kt Maven / Gradle / Ivy

The newest version!
package org.openrndr.extra.shapes.adjust

import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.extra.shapes.utilities.fromContours
import org.openrndr.extra.shapes.utilities.insertPointAt
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.Segment2D
import org.openrndr.shape.SegmentType
import org.openrndr.shape.ShapeContour
import kotlin.jvm.JvmRecord
import kotlin.math.abs

internal fun Vector2.transformedBy(t: Matrix44, mask: Int = 0x0f, maskRef: Int = 0x0f) =
    if ((mask and maskRef) != 0)
        (t * (this.xy01)).xy else {
        this
    }

fun  List.update(vararg updates: Pair): List {
    if (updates.isEmpty()) {
        return this
    }
    val result = this.toMutableList()
    for ((index, value) in updates) {
        result[index] = value
    }
    return result
}

/**
 * Helper for querying and adjusting [ShapeContour].
 * * An edge embodies exactly the same thing as a [Segment][org.openrndr.shape.Segment]
 * * All edge operations are immutable and will create a new [ContourEdge] pointing to a copied and updated [ShapeContour]
 * @param contour the contour to be adjusted
 * @param segmentIndex the index of the segment of the contour to be adjusted
 * @param adjustments a list of [SegmentOperation] that have been applied to reach to [contour], this is used to inform [ShapeContour]
 * of changes in the contour topology.
 * @since 0.4.4
 */
@JvmRecord
data class ContourEdge(
    val contour: ShapeContour,
    val segmentIndex: Int,
    val adjustments: List = emptyList()
) {
    /**
     * provide a copy without the list of adjustments
     */
    fun withoutAdjustments(): ContourEdge {
        return if (adjustments.isEmpty()) {
            this
        } else {
            copy(adjustments = emptyList())
        }
    }

    /**
     * convert the edge to a linear edge, truncating control points if those exist
     */
    fun toLinear(): ContourEdge {
        return if (contour.segments[segmentIndex].type != SegmentType.LINEAR) {
            val newSegment = contour.segments[segmentIndex].copy(control = emptyList())
            val newSegments = contour.segments
                .update(segmentIndex to newSegment)

            ContourEdge(
                ShapeContour.fromSegments(newSegments, contour.closed),
                segmentIndex
            )
        } else {
            this
        }
    }

    /**
     * convert the edge to a cubic edge
     */
    fun toCubic(): ContourEdge {
        return if (contour.segments[segmentIndex].type != SegmentType.CUBIC) {
            val newSegment = contour.segments[segmentIndex].cubic
            val newSegments = contour.segments
                .update(segmentIndex to newSegment)

            ContourEdge(
                ShapeContour.fromSegments(newSegments, contour.closed),
                segmentIndex
            )
        } else {
            this
        }
    }

    val length: Double
        get() {
            return contour.segments[segmentIndex].length
        }


    /**
     * replace this edge with a point at [t]
     * @param t an edge t value between 0 and 1
     */
    fun replacedWith(t: Double, updateTangents: Boolean): ContourEdge {
        if (contour.empty) {
            return withoutAdjustments()
        }
        val point = contour.segments[segmentIndex].position(t)
        val segmentInIndex = if (contour.closed) (segmentIndex - 1).mod(contour.segments.size) else segmentIndex - 1
        val segmentOutIndex = if (contour.closed) (segmentIndex + 1).mod(contour.segments.size) else segmentIndex + 1
        val refIn = contour.segments.getOrNull(segmentInIndex)
        val refOut = contour.segments.getOrNull(segmentOutIndex)

        val newSegments = contour.segments.toMutableList()
        if (refIn != null) {
            newSegments[segmentInIndex] = newSegments[segmentInIndex].copy(end = point)
        }
        if (refOut != null) {
            newSegments[segmentOutIndex] = newSegments[segmentOutIndex].copy(start = point)
        }
        val adjustments = newSegments.adjust {
            removeAt(segmentIndex)
        }
        return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)
    }

    fun splitIn(parts: Int): ContourEdge {
        if (contour.empty || parts < 2) {
            return withoutAdjustments()
        }
        val segment = contour.segments[segmentIndex]
        val r = segment.contour.rectified()
        val newSegments = (0..parts).map {
            it.toDouble() / parts
        }.windowed(2, 1).map {
            r.sub(it[0], it[1])
        }
        require(newSegments.size == parts)
        return replacedWith(ShapeContour.fromContours(newSegments, false, 1.0))
    }

    fun replacedWith(openContour: ShapeContour): ContourEdge {
        if (contour.empty) {
            return withoutAdjustments()
        }
        require(!openContour.closed) { "openContour should be open" }
        val segment = contour.segments[segmentIndex]
        var newSegments = contour.segments.toMutableList()

        var insertIndex = segmentIndex
        val adjustments = newSegments.adjust {
            removeAt(segmentIndex)

            if (segment.start.distanceTo(openContour.position(0.0)) > 1E-3) {
                add(insertIndex, Segment2D(segment.start, openContour.position(0.0)))
                insertIndex++
            }
            for (s in openContour.segments) {
                add(insertIndex, s)
                insertIndex++
            }
            if (segment.end.distanceTo(openContour.position(1.0)) > 1E-3) {
                add(insertIndex, Segment2D(segment.end, openContour.position(1.0)))
            }
        }
        return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)
    }


    /**
     * subs the edge from [t0] to [t1], preserves topology unless t0 = t1
     * @param t0 the start edge t-value, between 0 and 1
     * @param t1 the end edge t-value, between 0 and 1
     */
    fun subbed(t0: Double, t1: Double, updateTangents: Boolean = true): ContourEdge {
        if (contour.empty) {
            return withoutAdjustments()
        }
        if (abs(t0 - t1) > 1E-6) {
            val sub = contour.segments[segmentIndex].sub(t0, t1)
            val segmentInIndex = if (contour.closed) (segmentIndex - 1).mod(contour.segments.size) else segmentIndex - 1
            val segmentOutIndex =
                if (contour.closed) (segmentIndex + 1).mod(contour.segments.size) else segmentIndex + 1
            val refIn = contour.segments.getOrNull(segmentInIndex)
            val refOut = contour.segments.getOrNull(segmentOutIndex)

            val newSegments = contour.segments.toMutableList()
            if (refIn != null) {
                newSegments[segmentInIndex] = newSegments[segmentInIndex].copy(end = sub.start)
            }
            if (refOut != null) {
                newSegments[segmentOutIndex] = newSegments[segmentOutIndex].copy(start = sub.end)
            }
            newSegments[segmentIndex] = sub
            return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)
        } else {
            return replacedWith(t0, updateTangents)
        }
    }

    /**
     * split the edge at [t]
     * @param t an edge t value between 0 and 1, will not split when t == 0 or t == 1
     */
    fun splitAt(t: Double): ContourEdge {
        if (contour.empty) {
            return withoutAdjustments()
        }
        val newContour = contour.insertPointAt(segmentIndex, t)
        if (newContour.segments.size == contour.segments.size + 1) {
            return ContourEdge(newContour, segmentIndex, listOf(SegmentOperation.Insert(segmentIndex + 1, 1)))
        } else {
            return this.copy(adjustments = emptyList())
        }
    }


    enum class ControlMask(val mask: Int) {
        START(1),
        CONTROL0(2),
        CONTROL1(4),
        END(8)
    }

    fun maskOf(vararg control: ControlMask): Int {
        var mask = 0
        for (c in control) {
            mask = mask or c.mask
        }
        return mask
    }

    /**
     * apply [transform] to the edge
     * @param transform a [Matrix44]
     */
    fun transformedBy(
        transform: Matrix44,
        updateTangents: Boolean = true,
        mask: Int = 0xf,
        promoteToCubic: Boolean = false
    ): ContourEdge {
        val segment = contour.segments[segmentIndex].let { if (promoteToCubic) it.cubic else it }
        val newSegment = segment.copy(
            start = segment.start.transformedBy(transform, mask, ControlMask.START.mask),
            control = segment.control.mapIndexed { index, it -> it.transformedBy(transform, mask, 1 shl (index + 1)) },
            end = segment.end.transformedBy(transform, mask, ControlMask.END.mask)
        )
        val segmentInIndex = if (contour.closed) (segmentIndex - 1).mod(contour.segments.size) else segmentIndex - 1
        val segmentOutIndex = if (contour.closed) (segmentIndex + 1).mod(contour.segments.size) else segmentIndex + 1
        val refIn = contour.segments.getOrNull(segmentInIndex)
        val refOut = contour.segments.getOrNull(segmentOutIndex)

        val newSegments = contour.segments.map { it }.toMutableList()

        if (refIn != null) {
            var control = if (refIn.linear || !updateTangents) {
                refIn.control
            } else {
                refIn.cubic.control
            }
            if (control.isNotEmpty()) {
                control = listOf(control[0], control[1].transformedBy(transform))
            }
            newSegments[segmentInIndex] = refIn.copy(control = control, end = segment.start.transformedBy(transform))
        }
        if (refOut != null) {
            var control = if (refOut.linear || !updateTangents) {
                refOut.control
            } else {
                refOut.cubic.control
            }
            if (control.isNotEmpty()) {
                control = listOf(control[0].transformedBy(transform), control[1])

            }
            newSegments[segmentOutIndex] = refOut.copy(start = segment.end.transformedBy(transform), control = control)
        }

        newSegments[segmentIndex] = newSegment
        return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)
    }

    fun startMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge =
        transformedBy(buildTransform {
            translate(translation)
        }, updateTangents = updateTangents, mask = maskOf(ControlMask.START))

    fun control0MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform {
        translate(translation)
    }, updateTangents = false, mask = maskOf(ControlMask.CONTROL0), promoteToCubic = true)

    fun control1MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform {
        translate(translation)
    }, updateTangents = false, mask = maskOf(ControlMask.CONTROL1), promoteToCubic = true)

    fun endMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge {
        return transformedBy(buildTransform {
            translate(translation)
        }, updateTangents = updateTangents, mask = maskOf(ControlMask.END))
    }

    fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge {
        return transformedBy(buildTransform {
            translate(translation)
        }, updateTangents)
    }

    fun rotatedBy(rotationInDegrees: Double, anchorT: Double, updateTangents: Boolean = true): ContourEdge {
        val anchor = contour.segments[segmentIndex].position(anchorT)
        return transformedBy(buildTransform {
            translate(anchor)
            rotate(rotationInDegrees)
            translate(-anchor)
        }, updateTangents)
    }

    fun scaledBy(scaleFactor: Double, anchorT: Double, updateTangents: Boolean = true): ContourEdge {
        val anchor = contour.segments[segmentIndex].position(anchorT)
        return scaledBy(scaleFactor, anchor, updateTangents)
    }

    fun scaledBy(scaleFactor: Double, anchor: Vector2, updateTangents: Boolean = true): ContourEdge {
        return transformedBy(buildTransform {
            translate(anchor)
            scale(scaleFactor)
            translate(-anchor)
        }, updateTangents)
    }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy