[orx-shapes] Improve contour adjuster framework

This commit is contained in:
Edwin Jakobs
2024-01-22 15:22:20 +01:00
parent 0e8ca543ab
commit 44824e3d21
23 changed files with 498 additions and 15 deletions

View File

@@ -1,3 +1,5 @@
package org.openrndr.extra.shapes
import org.openrndr.math.Polar
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform

View File

@@ -2,7 +2,17 @@ package org.openrndr.extra.shapes.adjust
import org.openrndr.collections.pop
import org.openrndr.extra.shapes.vertex.ContourVertex
import org.openrndr.math.Vector2
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour
import kotlin.jvm.JvmName
class ContourAdjusterStatus(
val contour: ShapeContour,
val selectedSegments: List<Segment>,
val selectedPoints: List<Vector2>
)
/**
@@ -17,6 +27,7 @@ class ContourAdjuster(var contour: ShapeContour) {
var clearSelectedEdges: Boolean = false,
var clearSelectedVertices: Boolean = false
)
var parameters = Parameters()
val parameterStack = ArrayDeque<Parameters>()
@@ -75,6 +86,15 @@ class ContourAdjuster(var contour: ShapeContour) {
popVertexSelection()
}
val status: ContourAdjusterStatus
get() {
return ContourAdjusterStatus(contour,
edgeSelection.map { contour.segments[it] },
vertexSelection.map { if (it < contour.segments.size) contour.segments[it].start else contour.segments[it - 1].end }
)
}
/**
* the selected vertex
*/
@@ -149,7 +169,6 @@ class ContourAdjuster(var contour: ShapeContour) {
}
/**
* select multiple vertices using an index-vertex based [predicate]
*/
@@ -272,4 +291,13 @@ fun adjustContour(contour: ShapeContour, adjuster: ContourAdjuster.() -> Unit):
val ca = ContourAdjuster(contour)
ca.apply(adjuster)
return ca.contour
}
@JvmName("adjustContourSequenceStatus")
fun adjustContourSequence(
contour: ShapeContour,
adjuster: ContourAdjuster.() -> Sequence<ContourAdjusterStatus>
): Sequence<ContourAdjusterStatus> {
val ca = ContourAdjuster(contour)
return ca.adjuster()
}

View File

@@ -105,4 +105,13 @@ data class ContourAdjusterEdge(val contourAdjuster: ContourAdjuster, val segment
.subbed(t0, t1)
.contour
}
fun moveStartBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) }
fun moveControl0By(translation: Vector2) = wrap { control0MovedBy(translation) }
fun moveControl1By(translation: Vector2) = wrap { control1MovedBy(translation) }
fun moveEndBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) }
}

View File

@@ -4,7 +4,7 @@ import org.openrndr.extra.shapes.vertex.ContourVertex
import org.openrndr.math.Vector2
class ContourAdjusterVertex(val contourAdjuster: ContourAdjuster, val segmentIndex: () -> Int) {
private fun wrap(block: ContourVertex.() -> ContourVertex) {
fun wrap(block: ContourVertex.() -> ContourVertex) {
val newVertex = ContourVertex(contourAdjuster.contour, segmentIndex()).block()
contourAdjuster.contour = newVertex.contour
contourAdjuster.updateSelection(newVertex.adjustments)
@@ -43,7 +43,19 @@ class ContourAdjusterVertex(val contourAdjuster: ContourAdjuster, val segmentInd
contourAdjuster.selectVertex(segmentIndex())
}
val controlIn: Vector2?
get() = ContourVertex(contourAdjuster.contour, segmentIndex()).controlIn
val controlOut: Vector2?
get() = ContourVertex(contourAdjuster.contour, segmentIndex()).controlOut
fun remove(updateTangents: Boolean = true) = wrap { remove(updateTangents) }
fun moveControlInBy(translation: Vector2) = wrap { controlInMovedBy(translation) }
fun moveControlOutBy(translation: Vector2) = wrap { controlOutMovedBy(translation) }
fun moveBy(translation: Vector2, updateTangents: Boolean = true) = wrap { movedBy(translation, updateTangents) }
fun moveTo(position: Vector2, updateTangents: Boolean = true) = wrap { movedBy(position - this.position, updateTangents) }
fun rotate(rotationInDegrees: Double) = wrap { rotatedBy(rotationInDegrees) }

View File

@@ -11,7 +11,12 @@ import org.openrndr.shape.SegmentType
import org.openrndr.shape.ShapeContour
import kotlin.math.abs
internal fun Vector2.transformedBy(t: Matrix44) = (t * (this.xy01)).xy
internal fun Vector2.transformedBy(t: Matrix44, mask: Int = 0x0f, maskRef: Int = 0x0f) =
if ((mask and maskRef) != 0)
(t * (this.xy01)).xy else {
this
}
fun <E> List<E>.update(vararg updates: Pair<Int, E>): List<E> {
if (updates.isEmpty()) {
return this
@@ -209,16 +214,36 @@ data class ContourEdge(
}
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): ContourEdge {
val segment = contour.segments[segmentIndex]
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),
control = segment.control.map { it.transformedBy(transform) },
end = segment.end.transformedBy(transform)
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
@@ -255,6 +280,25 @@ data class ContourEdge(
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)

View File

@@ -47,6 +47,33 @@ data class ContourVertex(
}
}
val controlIn: Vector2?
get() {
return if (contour.closed || (segmentIndex > 0 && segmentIndex < contour.segments.size)) {
contour.segments[(segmentIndex-1).mod(contour.segments.size)].cubic.control[1]
} else if (segmentIndex == 0) {
null
} else {
contour.segments[segmentIndex-1].cubic.control[1]
}
}
val controlOut: Vector2?
get() {
return if (contour.closed || segmentIndex < contour.segments.size) {
contour.segments[segmentIndex].cubic.control[0]
} else {
null
}
}
val tangentIn: Vector2?
get() = controlIn?.minus(position)
val tangentOut: Vector2?
get() = controlOut?.minus(position)
fun remove(updateTangents: Boolean = true): ContourVertex {
if (contour.empty) {
return withoutAdjustments()
@@ -67,6 +94,27 @@ data class ContourVertex(
return ContourVertex(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)
}
fun controlInMovedBy(translation: Vector2): ContourVertex {
if (contour.empty) {
return withoutAdjustments()
}
val transform = buildTransform {
translate(translation)
}
return transformTangents(transform, Matrix44.IDENTITY)
}
fun controlOutMovedBy(translation: Vector2): ContourVertex {
if (contour.empty) {
return withoutAdjustments()
}
val transform = buildTransform {
translate(translation)
}
return transformTangents(Matrix44.IDENTITY, transform)
}
fun scaledBy(scaleFactor: Double): ContourVertex {
if (contour.empty) {
return withoutAdjustments()
@@ -96,13 +144,16 @@ data class ContourVertex(
return withoutAdjustments()
}
val newSegments = contour.segments.map { it }.toMutableList()
val refOut = contour.segments[segmentIndex]
val refOut = contour.segments.getOrNull(segmentIndex)
val refIn = if (contour.closed) contour.segments[(segmentIndex - 1).mod(contour.segments.size)] else
contour.segments.getOrNull(segmentIndex - 1)
newSegments[segmentIndex] = run {
val cubicSegment = refOut.cubic
val newControls = listOf((transformOut * cubicSegment.control[0].xy01).xy, cubicSegment.control[1])
refOut.copy(control = newControls)
if (refOut != null) {
newSegments[segmentIndex] = run {
val cubicSegment = refOut.cubic
val newControls = listOf((transformOut * cubicSegment.control[0].xy01).xy, cubicSegment.control[1])
refOut.copy(control = newControls)
}
}
val segmentIndexIn = (segmentIndex - 1).mod(contour.segments.size)
if (refIn != null) {

View File

@@ -0,0 +1,22 @@
package org.openrndr.extra.shapes.adjust.extensions
import org.openrndr.extra.shapes.adjust.ContourAdjusterVertex
import org.openrndr.extra.shapes.vertex.ContourVertex
fun ContourVertex.tangentsAveraged(): ContourVertex {
if (contour.empty || tangentIn == null || tangentOut == null) return withoutAdjustments()
val sum = (tangentIn!! - tangentOut!!).normalized
val lengthIn = tangentIn!!.length
val lengthOut = tangentOut!!.length
val positionIn = position + sum * lengthIn
val positionOut = position - sum * lengthOut
return controlOutMovedBy(positionOut - controlOut!!).controlInMovedBy(positionIn - controlIn!!)
}
/**
* Average the in and out tangents
*/
fun ContourAdjusterVertex.averageTangents() = wrap { tangentsAveraged() }

View File

@@ -0,0 +1,39 @@
package org.openrndr.extra.shapes.adjust.extensions
import org.openrndr.extra.shapes.adjust.ContourAdjusterVertex
import org.openrndr.extra.shapes.vertex.ContourVertex
import org.openrndr.math.transforms.buildTransform
fun ContourVertex.tangentInReflectedToOut(tangentScale: Double = 1.0): ContourVertex {
if (contour.empty || tangentIn == null || tangentOut == null) return withoutAdjustments()
return controlOutMovedBy(position - tangentIn!! * tangentScale - controlOut!!)
}
fun ContourVertex.tangentOutReflectedToIn(tangentScale: Double = 1.0): ContourVertex {
if (contour.empty || tangentIn == null || tangentOut == null) return withoutAdjustments()
return controlInMovedBy(position - tangentOut!! * tangentScale - controlIn!!)
}
fun ContourVertex.switchedTangents(preserveLength: Boolean = false): ContourVertex {
if (contour.empty || tangentIn == null || tangentOut == null) return withoutAdjustments()
val sIn = if (preserveLength) tangentIn!!.length / tangentOut!!.length else 1.0
val sOut = if (preserveLength) 1.0 / sIn else 1.0
val newControlIn = position + tangentOut!! * sIn
val newControlOut = position + tangentIn!! * sOut
return transformTangents(
buildTransform { translate(newControlIn - controlIn!!) },
buildTransform { translate(newControlOut - controlOut!!) })
}
/**
* Switch in and out tangents
*/
fun ContourAdjusterVertex.switchTangents(preserveLength: Boolean = false) = wrap { switchedTangents(preserveLength) }
fun ContourAdjusterVertex.reflectTangentInToOut(tangentScale: Double) = wrap { tangentInReflectedToOut(tangentScale) }
fun ContourAdjusterVertex.reflectTangentOutToIn(tangentScale: Double) = wrap { tangentOutReflectedToIn(tangentScale) }

View File

@@ -58,6 +58,7 @@ fun Segment.withTunniPoint(tunniPoint: Vector2): Segment {
* @since orx 0.4.5
*/
fun Segment.withTunniLine(pointOnLine: Vector2): Segment {
println("hoi! $pointOnLine")
val ls = LineSegment(pointOnLine, pointOnLine + this.cubic.control[0] - this.cubic.control[1])
val ac0 = LineSegment(start, this.cubic.control[0])
val bc1 = LineSegment(end, this.cubic.control[1])
@@ -66,6 +67,10 @@ fun Segment.withTunniLine(pointOnLine: Vector2): Segment {
val cp1 = intersection(ls, bc1, Double.POSITIVE_INFINITY)
return if (cp0 != Vector2.INFINITY && cp1 != Vector2.INFINITY) {
println("$cp0 $cp1")
copy(start, listOf(cp0, cp1), end)
} else this
} else {
println("${cp0} ${cp1}")
this
}
}

View File

@@ -44,7 +44,7 @@ fun ContourEdge.withTunniLine(pointOnLine: Vector2): ContourEdge {
if (contour.empty) {
return withoutAdjustments()
} else {
val segment = contour.segments[segmentIndex].withTunniPoint(pointOnLine)
val segment = contour.segments[segmentIndex].withTunniLine(pointOnLine)
val newSegments = contour.segments.map { it }.toMutableList()
newSegments[segmentIndex] = segment
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)