286 lines
11 KiB
Kotlin
286 lines
11 KiB
Kotlin
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.Segment
|
|
import org.openrndr.shape.SegmentType
|
|
import org.openrndr.shape.ShapeContour
|
|
import kotlin.math.abs
|
|
|
|
internal fun Vector2.transformedBy(t: Matrix44) = (t * (this.xy01)).xy
|
|
fun <E> List<E>.update(vararg updates: Pair<Int, E>): List<E> {
|
|
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
|
|
*/
|
|
data class ContourEdge(
|
|
val contour: ShapeContour,
|
|
val segmentIndex: Int,
|
|
val adjustments: List<SegmentOperation> = 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 = emptyArray())
|
|
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, Segment(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, Segment(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())
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* apply [transform] to the edge
|
|
* @param transform a [Matrix44]
|
|
*/
|
|
fun transformedBy(transform: Matrix44, updateTangents: Boolean = true): ContourEdge {
|
|
val segment = contour.segments[segmentIndex]
|
|
val newSegment = segment.copy(
|
|
start = segment.start.transformedBy(transform),
|
|
control = segment.control.map { it.transformedBy(transform) }.toTypedArray<Vector2>(),
|
|
end = segment.end.transformedBy(transform)
|
|
)
|
|
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) {
|
|
val control = if (refIn.linear || !updateTangents) {
|
|
refIn.control
|
|
} else {
|
|
refIn.cubic.control
|
|
}
|
|
if (control.isNotEmpty()) {
|
|
control[1] = control[1].transformedBy(transform)
|
|
}
|
|
newSegments[segmentInIndex] = refIn.copy(end = segment.start.transformedBy(transform))
|
|
}
|
|
if (refOut != null) {
|
|
val control = if (refOut.linear || !updateTangents) {
|
|
refOut.control
|
|
} else {
|
|
refOut.cubic.control
|
|
}
|
|
if (control.isNotEmpty()) {
|
|
control[0] = control[0].transformedBy(transform)
|
|
}
|
|
newSegments[segmentOutIndex] = refOut.copy(start = segment.end.transformedBy(transform))
|
|
}
|
|
|
|
newSegments[segmentIndex] = newSegment
|
|
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|