[orx-shapes] Add WIP ContourAdjuster framework
This commit is contained in:
159
orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt
Normal file
159
orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt
Normal file
@@ -0,0 +1,159 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
import org.openrndr.extra.shapes.vertex.ContourVertex
|
||||
import org.openrndr.shape.ShapeContour
|
||||
|
||||
/**
|
||||
* Adjusts [ShapeContour] using an accessible interface.
|
||||
*
|
||||
* [ContourAdjuster]
|
||||
*/
|
||||
class ContourAdjuster(var contour: ShapeContour) {
|
||||
/**
|
||||
* selected vertex indices
|
||||
*/
|
||||
var vertexIndices = listOf(0)
|
||||
|
||||
/**
|
||||
* selected edge indices
|
||||
*/
|
||||
var edgeIndices = listOf(0)
|
||||
|
||||
/**
|
||||
* the selected vertex
|
||||
*/
|
||||
val vertex: ContourAdjusterVertex
|
||||
get() {
|
||||
return ContourAdjusterVertex(this, vertexIndices.first())
|
||||
}
|
||||
|
||||
/**
|
||||
* the selected edge
|
||||
*/
|
||||
val edge: ContourAdjusterEdge
|
||||
get() {
|
||||
return ContourAdjusterEdge(this, edgeIndices.first())
|
||||
}
|
||||
|
||||
/**
|
||||
* select a vertex by index
|
||||
*/
|
||||
fun selectVertex(index: Int) {
|
||||
vertexIndices = listOf(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* deselect a vertex by index
|
||||
*/
|
||||
fun deselectVertex(index: Int) {
|
||||
vertexIndices = vertexIndices.filter { it != index }
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple vertices
|
||||
*/
|
||||
fun selectVertices(vararg indices: Int) {
|
||||
vertexIndices = indices.toList().distinct()
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple vertices using an index based [predicate]
|
||||
*/
|
||||
fun selectVertices(predicate: (Int) -> Boolean) {
|
||||
vertexIndices =
|
||||
(0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter(predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple vertices using an index-vertex based [predicate]
|
||||
*/
|
||||
fun selectVertices(predicate: (Int, ContourVertex) -> Boolean) {
|
||||
vertexIndices =
|
||||
(0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter { index ->
|
||||
predicate(index, ContourVertex(contour, index) )
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* select an edge by index
|
||||
*/
|
||||
fun selectEdge(index: Int) {
|
||||
selectEdges(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple edges by index
|
||||
*/
|
||||
fun selectEdges(vararg indices: Int) {
|
||||
edgeIndices = indices.toList().distinct()
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple vertices using an index based [predicate]
|
||||
*/
|
||||
fun selectEdges(predicate: (Int) -> Boolean) {
|
||||
edgeIndices =
|
||||
contour.segments.indices.filter(predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* select multiple edges using an index-edge based [predicate]
|
||||
*/
|
||||
fun selectEdges(predicate: (Int, ContourEdge) -> Boolean) {
|
||||
vertexIndices =
|
||||
(0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter { index ->
|
||||
predicate(index, ContourEdge(contour, index) )
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelection(adjustments: List<SegmentOperation>) {
|
||||
var newVertexIndices = vertexIndices
|
||||
var newEdgeIndices = edgeIndices
|
||||
|
||||
for (adjustment in adjustments) {
|
||||
when (adjustment) {
|
||||
is SegmentOperation.Insert -> {
|
||||
fun insert(list: List<Int>) = list.map {
|
||||
if (it >= adjustment.index) {
|
||||
it + adjustment.amount
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
newVertexIndices = insert(newVertexIndices)
|
||||
newEdgeIndices = insert(newEdgeIndices)
|
||||
}
|
||||
is SegmentOperation.Remove -> {
|
||||
// TODO: handling of vertices in open contours is wrong here
|
||||
newVertexIndices = newVertexIndices.mapNotNull {
|
||||
if (it in adjustment.index ..< adjustment.index+adjustment.amount) {
|
||||
null
|
||||
} else if (it > adjustment.index) {
|
||||
it - adjustment.amount
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
newEdgeIndices = newEdgeIndices.mapNotNull {
|
||||
if (it in adjustment.index ..< adjustment.index+adjustment.amount) {
|
||||
null
|
||||
} else if (it > adjustment.index) {
|
||||
it - adjustment.amount
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a contour adjuster
|
||||
*/
|
||||
fun adjustContour(contour: ShapeContour, adjuster: ContourAdjuster.() -> Unit): ShapeContour {
|
||||
val ca = ContourAdjuster(contour)
|
||||
ca.apply(adjuster)
|
||||
return ca.contour
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
import org.openrndr.math.Vector2
|
||||
|
||||
data class ContourAdjusterEdge(val contourAdjuster: ContourAdjuster, val segmentIndex: Int) {
|
||||
|
||||
/**
|
||||
* A [ContourAdjusterVertex] interface for the start-vertex of the edge
|
||||
*/
|
||||
val start
|
||||
get() = ContourAdjusterVertex(contourAdjuster, segmentIndex)
|
||||
|
||||
/**
|
||||
* A [ContourAdjusterVertex] interface for the end-vertex of the edge
|
||||
*/
|
||||
val end
|
||||
get() = ContourAdjusterVertex(contourAdjuster, (segmentIndex + 1).mod(contourAdjuster.contour.segments.size))
|
||||
|
||||
/**
|
||||
* A link to the edge before this edge
|
||||
*/
|
||||
val previous: ContourAdjusterEdge?
|
||||
get() = if (contourAdjuster.contour.closed) {
|
||||
this.copy(segmentIndex = (segmentIndex - 1).mod(contourAdjuster.contour.segments.size))
|
||||
} else {
|
||||
if (segmentIndex > 0) {
|
||||
this.copy(segmentIndex = segmentIndex - 1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A link to the edge after this edge
|
||||
*/
|
||||
val next: ContourAdjusterEdge?
|
||||
get() = if (contourAdjuster.contour.closed) {
|
||||
this.copy(segmentIndex = (segmentIndex + 1).mod(contourAdjuster.contour.segments.size))
|
||||
} else {
|
||||
if (segmentIndex < contourAdjuster.contour.segments.size - 1) {
|
||||
this.copy(segmentIndex = segmentIndex + 1)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun select() {
|
||||
contourAdjuster.selectEdge(segmentIndex)
|
||||
}
|
||||
private fun wrap(block: ContourEdge.() -> ContourEdge) {
|
||||
val newEdge = ContourEdge(contourAdjuster.contour, segmentIndex).block()
|
||||
contourAdjuster.contour = newEdge.contour
|
||||
contourAdjuster.updateSelection(newEdge.adjustments)
|
||||
}
|
||||
fun toCubic() = wrap { toCubic() }
|
||||
fun splitAt(t: Double) = wrap { splitAt(t) }
|
||||
fun moveBy(translation: Vector2, updateTangents: Boolean = true) = wrap { movedBy(translation, updateTangents) }
|
||||
fun rotate(rotationInDegrees: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) =
|
||||
wrap { rotatedBy(rotationInDegrees, anchorT, updateTangents) }
|
||||
fun scale(scaleFactor: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) =
|
||||
wrap { scaledBy(scaleFactor, anchorT, updateTangents = true) }
|
||||
|
||||
fun replaceWith(t:Double, updateTangents: Boolean = true) {
|
||||
wrap { replacedWith(t, updateTangents) }
|
||||
}
|
||||
|
||||
fun sub(t0:Double, t1: Double, updateTangents: Boolean = true) {
|
||||
contourAdjuster.contour =
|
||||
ContourEdge(contourAdjuster.contour, segmentIndex)
|
||||
.subbed(t0, t1)
|
||||
.contour
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
/*
|
||||
A collection of extension functions for ContourAdjuster. It is encouraged to keep the ContourAdjuster class at a minimum
|
||||
size by adding extension functions here.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Apply a sub to the subject contour
|
||||
*/
|
||||
fun ContourAdjuster.sub(t0: Double, t1: Double, updateSelection: Boolean = true) {
|
||||
val oldSegments = contour.segments
|
||||
contour = contour.sub(t0, t1)
|
||||
val newSegments = contour.segments
|
||||
|
||||
// TODO: this update of the selections is not right
|
||||
if (updateSelection && oldSegments.size != newSegments.size) {
|
||||
selectEdges()
|
||||
selectVertices()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
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) {
|
||||
val newVertex = ContourVertex(contourAdjuster.contour, segmentIndex).block()
|
||||
contourAdjuster.contour = newVertex.contour
|
||||
contourAdjuster.updateSelection(newVertex.adjustments)
|
||||
}
|
||||
fun select() {
|
||||
contourAdjuster.selectVertex(segmentIndex)
|
||||
}
|
||||
fun remove(updateTangents: Boolean = true) = wrap { remove(updateTangents) }
|
||||
fun moveBy(translation: Vector2, updateTangents: Boolean = true) = wrap { movedBy(translation, updateTangents) }
|
||||
fun rotate(rotationInDegrees: Double) = wrap { rotatedBy(rotationInDegrees) }
|
||||
fun scale(scaleFactor: Double) = wrap { scaledBy(scaleFactor) }
|
||||
|
||||
}
|
||||
186
orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt
Normal file
186
orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt
Normal file
@@ -0,0 +1,186 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
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.SegmentType
|
||||
import org.openrndr.shape.ShapeContour
|
||||
import kotlin.math.abs
|
||||
|
||||
private 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]
|
||||
*/
|
||||
data class ContourEdge(
|
||||
val contour: ShapeContour,
|
||||
val segmentIndex: Int,
|
||||
val adjustments: List<SegmentOperation> = emptyList()
|
||||
) {
|
||||
fun withoutAdjustments(): ContourEdge {
|
||||
return if (adjustments.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
copy(adjustments = emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
fun toCubic(): ContourEdge {
|
||||
if (contour.segments[segmentIndex].type != SegmentType.CUBIC) {
|
||||
val newSegment = contour.segments[segmentIndex].cubic
|
||||
val newSegments = contour.segments
|
||||
.update(segmentIndex to newSegment)
|
||||
|
||||
return ContourEdge(
|
||||
ShapeContour.fromSegments(newSegments, contour.closed),
|
||||
segmentIndex
|
||||
)
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
control[1] = control[1].transformedBy(transform)
|
||||
newSegments[segmentInIndex] = refIn.copy(end = segment.start.transformedBy(transform), control = control)
|
||||
}
|
||||
if (refOut != null) {
|
||||
val control = if (refOut.linear || !updateTangents) {
|
||||
refOut.control
|
||||
} else {
|
||||
refOut.cubic.control
|
||||
}
|
||||
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 transformedBy(buildTransform {
|
||||
translate(anchor)
|
||||
scale(scaleFactor)
|
||||
translate(-anchor)
|
||||
}, updateTangents)
|
||||
}
|
||||
}
|
||||
|
||||
132
orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt
Normal file
132
orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.openrndr.extra.shapes.vertex
|
||||
|
||||
import org.openrndr.extra.shapes.adjust.ContourAdjuster
|
||||
import org.openrndr.extra.shapes.adjust.SegmentAdjustments
|
||||
import org.openrndr.extra.shapes.adjust.SegmentOperation
|
||||
import org.openrndr.extra.shapes.adjust.adjust
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.transforms.buildTransform
|
||||
import org.openrndr.shape.ShapeContour
|
||||
|
||||
data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val adjustments: List<SegmentOperation> = emptyList()) {
|
||||
fun withoutAdjustments() : ContourVertex {
|
||||
return if (adjustments.isEmpty()) {
|
||||
this
|
||||
} else {
|
||||
copy(adjustments = emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
val position: Vector2
|
||||
get() {
|
||||
return contour.segments[segmentIndex].start
|
||||
}
|
||||
|
||||
fun remove(updateTangents: Boolean = true) : ContourVertex {
|
||||
if (contour.empty) {
|
||||
return withoutAdjustments()
|
||||
}
|
||||
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 newSegments = contour.segments.map { it }.toMutableList()
|
||||
val refIn = newSegments.getOrNull(segmentInIndex)
|
||||
val refOut = newSegments.getOrNull(segmentOutIndex)
|
||||
|
||||
val segment = newSegments[segmentIndex]
|
||||
if (refIn != null) {
|
||||
newSegments[segmentInIndex] = refIn.copy(end = segment.end)
|
||||
}
|
||||
val adjustments = newSegments.adjust {
|
||||
removeAt(segmentIndex)
|
||||
}
|
||||
return ContourVertex(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)
|
||||
}
|
||||
|
||||
fun scaledBy(scaleFactor: Double): ContourVertex {
|
||||
if (contour.empty) {
|
||||
return withoutAdjustments()
|
||||
}
|
||||
val transform = buildTransform {
|
||||
translate(position)
|
||||
this.scale(scaleFactor)
|
||||
translate(-position)
|
||||
}
|
||||
return transformTangents(transform, transform)
|
||||
}
|
||||
|
||||
fun rotatedBy(rotationInDegrees: Double): ContourVertex {
|
||||
if (contour.empty) {
|
||||
return withoutAdjustments()
|
||||
}
|
||||
val transform = buildTransform {
|
||||
translate(position)
|
||||
this.rotate(rotationInDegrees)
|
||||
translate(-position)
|
||||
}
|
||||
return transformTangents(transform, transform)
|
||||
}
|
||||
|
||||
fun transformTangents(transformIn: Matrix44, transformOut: Matrix44 = transformIn): ContourVertex {
|
||||
if (contour.empty) {
|
||||
return withoutAdjustments()
|
||||
}
|
||||
val newSegments = contour.segments.map { it }.toMutableList()
|
||||
val refOut = contour.segments[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 = arrayOf((transformOut * cubicSegment.control[0].xy01).xy, cubicSegment.control[1])
|
||||
refOut.copy(control = newControls)
|
||||
}
|
||||
val segmentIndexIn = (segmentIndex - 1).mod(contour.segments.size)
|
||||
if (refIn != null) {
|
||||
newSegments[segmentIndexIn] = run {
|
||||
val cubicSegment = refIn.cubic
|
||||
val newControls = arrayOf(cubicSegment.control[0], (transformIn * cubicSegment.control[1].xy01).xy)
|
||||
refIn.copy(control = newControls)
|
||||
}
|
||||
}
|
||||
val newContour = ShapeContour.fromSegments(newSegments, contour.closed, contour.polarity)
|
||||
|
||||
return ContourVertex(newContour, segmentIndex)
|
||||
|
||||
}
|
||||
|
||||
fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourVertex {
|
||||
if (contour.empty) {
|
||||
return withoutAdjustments()
|
||||
}
|
||||
|
||||
val newSegments = contour.segments.map { it }.toMutableList()
|
||||
val refOut = contour.segments[segmentIndex]
|
||||
val refIn = if (contour.closed) contour.segments[(segmentIndex - 1).mod(contour.segments.size)] else
|
||||
contour.segments.getOrNull(segmentIndex - 1)
|
||||
val newPosition = refOut.start + translation
|
||||
newSegments[segmentIndex] = if (updateTangents && !refOut.linear) {
|
||||
val cubicSegment = refOut.cubic
|
||||
val newControls = arrayOf(cubicSegment.control[0] + translation, cubicSegment.control[1])
|
||||
refOut.copy(start = newPosition, control = newControls)
|
||||
} else {
|
||||
newSegments[segmentIndex].copy(start = newPosition)
|
||||
}
|
||||
|
||||
val segmentIndexIn = (segmentIndex - 1).mod(contour.segments.size)
|
||||
if (refIn != null) {
|
||||
newSegments[segmentIndexIn] =
|
||||
if (updateTangents && !refIn.linear) {
|
||||
val cubicSegment = refIn.cubic
|
||||
val newControls = arrayOf(cubicSegment.control[0], cubicSegment.control[1] + translation)
|
||||
newSegments[segmentIndexIn].copy(control = newControls, end = newPosition)
|
||||
} else {
|
||||
|
||||
newSegments[segmentIndexIn].copy(end = newPosition)
|
||||
}
|
||||
}
|
||||
val newContour = ShapeContour.fromSegments(newSegments, contour.closed, contour.polarity)
|
||||
|
||||
return ContourVertex(newContour, segmentIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.openrndr.extra.shapes.adjust
|
||||
|
||||
import org.openrndr.shape.Segment
|
||||
|
||||
sealed interface SegmentOperation {
|
||||
class Remove(val index: Int, val amount: Int) : SegmentOperation
|
||||
class Insert(val index: Int, val amount: Int) : SegmentOperation
|
||||
}
|
||||
|
||||
|
||||
class SegmentAdjustments(
|
||||
val replacements: List<Triple<Int, Segment, Segment>>,
|
||||
val operations: List<SegmentOperation>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = SegmentAdjustments(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
class SegmentAdjuster(val list: MutableList<Segment>) {
|
||||
val adjustments = mutableListOf<SegmentOperation>()
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
list.removeAt(index)
|
||||
adjustments.add(SegmentOperation.Remove(index, 1))
|
||||
}
|
||||
fun add(segment: Segment) {
|
||||
list.add(segment)
|
||||
adjustments.add(SegmentOperation.Insert(list.lastIndex, 1))
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableList<Segment>.adjust(block: SegmentAdjuster.() -> Unit) : List<SegmentOperation> {
|
||||
val adjuster = SegmentAdjuster(this)
|
||||
adjuster.block()
|
||||
return adjuster.adjustments
|
||||
}
|
||||
28
orx-shapes/src/commonMain/kotlin/utilities/FromContours.kt
Normal file
28
orx-shapes/src/commonMain/kotlin/utilities/FromContours.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.openrndr.extra.shapes.utilities
|
||||
|
||||
import org.openrndr.shape.ShapeContour
|
||||
import org.openrndr.shape.contour
|
||||
|
||||
/**
|
||||
* Create a contour from a list of contours
|
||||
*/
|
||||
fun ShapeContour.Companion.fromContours(contours: List<ShapeContour>, closed: Boolean, connectEpsilon:Double=1E-6) : ShapeContour {
|
||||
val contours = contours.filter { !it.empty }
|
||||
if (contours.isEmpty()) {
|
||||
return EMPTY
|
||||
}
|
||||
return contour {
|
||||
moveTo(contours.first().position(0.0))
|
||||
for (c in contours.windowed(2,1,true)) {
|
||||
copy(c[0])
|
||||
if (c.size == 2) {
|
||||
if (c[0].position(1.0).distanceTo(c[1].position(0.0)) > connectEpsilon ) {
|
||||
lineTo(c[1].position(0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (closed) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
35
orx-shapes/src/commonMain/kotlin/utilities/InsertPoint.kt
Normal file
35
orx-shapes/src/commonMain/kotlin/utilities/InsertPoint.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package org.openrndr.extra.shapes.utilities
|
||||
|
||||
import org.openrndr.shape.ShapeContour
|
||||
|
||||
/**
|
||||
* Insert point at [t]
|
||||
* @param ascendingTs a list of ascending T values
|
||||
* @param weldEpsilon minimum distance between T values
|
||||
*/
|
||||
fun ShapeContour.insertPointAt(t: Double, weldEpsilon: Double = 1E-6): ShapeContour {
|
||||
val splitContours = splitAt(listOf(t), weldEpsilon)
|
||||
return ShapeContour.fromContours(splitContours, closed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert point at [segmentIndex], [segmentT]
|
||||
* @param weldEpsilon minimum distance between T values
|
||||
*/
|
||||
fun ShapeContour.insertPointAt(segmentIndex: Int, segmentT: Double, weldEpsilon: Double = 1E-6): ShapeContour {
|
||||
val t = (1.0 / segments.size) * (segmentIndex + segmentT)
|
||||
val splitContours = splitAt(listOf(t), weldEpsilon)
|
||||
|
||||
return ShapeContour.fromContours(splitContours, closed)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert points at [ascendingTs]
|
||||
* @param ascendingTs a list of ascending T values
|
||||
* @param weldEpsilon minimum distance between T values
|
||||
*/
|
||||
fun ShapeContour.insertPointsAt(ascendingTs: List<Double>, weldEpsilon:Double = 1E-6) : ShapeContour {
|
||||
val splitContours = splitAt(ascendingTs, weldEpsilon)
|
||||
return ShapeContour.fromContours(splitContours, closed)
|
||||
}
|
||||
30
orx-shapes/src/commonMain/kotlin/utilities/SplitAt.kt
Normal file
30
orx-shapes/src/commonMain/kotlin/utilities/SplitAt.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package org.openrndr.extra.shapes.utilities
|
||||
|
||||
import org.openrndr.shape.Segment
|
||||
import org.openrndr.shape.ShapeContour
|
||||
|
||||
fun ShapeContour.splitAt(segmentIndex: Double, segmentT: Double): List<ShapeContour> {
|
||||
val t = (1.0 / segments.size) * (segmentIndex + segmentT)
|
||||
return splitAt(listOf(t))
|
||||
}
|
||||
|
||||
fun ShapeContour.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<ShapeContour> {
|
||||
if (empty || ascendingTs.isEmpty()) {
|
||||
return listOf(this)
|
||||
}
|
||||
@Suppress("NAME_SHADOWING") val ascendingTs = (listOf(0.0) + ascendingTs + listOf(1.0)).weldAscending(weldEpsilon)
|
||||
return ascendingTs.windowed(2, 1).map {
|
||||
sub(it[0], it[1])
|
||||
}
|
||||
}
|
||||
|
||||
fun Segment.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<Segment> {
|
||||
if (ascendingTs.isEmpty()) {
|
||||
return listOf(this)
|
||||
}
|
||||
|
||||
@Suppress("NAME_SHADOWING") val ascendingTs = (listOf(0.0) + ascendingTs + listOf(1.0)).weldAscending(weldEpsilon)
|
||||
return ascendingTs.windowed(2, 1).map {
|
||||
sub(it[0], it[1])
|
||||
}
|
||||
}
|
||||
21
orx-shapes/src/commonMain/kotlin/utilities/WeldAscending.kt
Normal file
21
orx-shapes/src/commonMain/kotlin/utilities/WeldAscending.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package org.openrndr.extra.shapes.utilities
|
||||
|
||||
/**
|
||||
* Weld values if their distance is less than [epsilon]
|
||||
*/
|
||||
fun List<Double>.weldAscending(epsilon: Double = 1E-6): List<Double> {
|
||||
return if (size <= 1) {
|
||||
this
|
||||
} else {
|
||||
val result = mutableListOf(first())
|
||||
var lastPassed = first()
|
||||
for (i in 1 until size) {
|
||||
require(this[i] >= lastPassed) { "input list is not in ascending order" }
|
||||
if (this[i] - lastPassed > epsilon) {
|
||||
result.add(this[i])
|
||||
lastPassed = this[i]
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
33
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour01.kt
Normal file
33
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour01.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.adjust.adjustContour
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
var contour = Circle(drawer.bounds.center, 300.0).contour
|
||||
contour = adjustContour(contour) {
|
||||
selectVertex(0)
|
||||
vertex.moveBy(Vector2(cos(seconds) * 40.0, sin(seconds * 0.43) * 40.0))
|
||||
|
||||
selectVertex(2)
|
||||
vertex.rotate(seconds * 45.0)
|
||||
|
||||
selectVertex(1)
|
||||
vertex.scale(cos(seconds*0.943)*2.0)
|
||||
}
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(contour)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour02.kt
Normal file
32
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour02.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.adjust.adjustContour
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
var contour = Circle(drawer.bounds.center, 300.0).contour
|
||||
contour = adjustContour(contour) {
|
||||
selectVertex(0)
|
||||
vertex.remove()
|
||||
selectVertex(0)
|
||||
vertex.moveBy(Vector2(cos(seconds) * 40.0, sin(seconds * 0.43) * 40.0))
|
||||
vertex.scale(cos(seconds*2.0)*2.0)
|
||||
|
||||
|
||||
}
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(contour)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour03.kt
Normal file
42
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour03.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.adjust.adjustContour
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
var contour = drawer.bounds.scaledBy(0.5, 0.5, 0.5).contour
|
||||
contour = adjustContour(contour) {
|
||||
for (i in 0 until 4) {
|
||||
selectEdge(i)
|
||||
edge.toCubic()
|
||||
}
|
||||
selectEdge(0)
|
||||
edge.scale(0.5, 0.5)
|
||||
edge.rotate(cos(seconds*0.5)*30.0)
|
||||
selectEdge(1)
|
||||
edge.toCubic()
|
||||
edge.splitAt(0.5)
|
||||
edge.moveBy(Vector2(cos(seconds*10.0) * 40.0, 0.0))
|
||||
//edge.next?.select()
|
||||
selectEdge(3)
|
||||
|
||||
edge.moveBy(Vector2(0.0, sin(seconds*10.0) * 40.0))
|
||||
|
||||
|
||||
}
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(contour)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour04.kt
Normal file
31
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour04.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.adjust.adjustContour
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
var contour = if (seconds.mod(2.0) < 1.0) {
|
||||
drawer.bounds.scaledBy(0.5, 0.5, 0.5).contour
|
||||
} else {
|
||||
Circle(drawer.bounds.center, 300.0).contour
|
||||
}
|
||||
contour = adjustContour(contour) {
|
||||
selectEdge(0)
|
||||
edge.replaceWith(cos(seconds) * 0.5 + 0.5)
|
||||
}
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(contour)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour05.kt
Normal file
31
orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour05.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.shapes.adjust.adjustContour
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
extend {
|
||||
var contour =
|
||||
Circle(drawer.bounds.center, 300.0).contour
|
||||
|
||||
contour = adjustContour(contour) {
|
||||
for (i in 0 until 4) {
|
||||
selectEdge(i)
|
||||
edge.sub(0.2, 0.8)
|
||||
}
|
||||
}
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(contour)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
orx-shapes/src/jvmDemo/kotlin/DemoSplit01.kt
Normal file
28
orx-shapes/src/jvmDemo/kotlin/DemoSplit01.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.color.presets.MEDIUM_PURPLE
|
||||
import org.openrndr.extra.shapes.utilities.splitAt
|
||||
import org.openrndr.shape.Circle
|
||||
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
|
||||
val c = Circle(drawer.bounds.center, 300.0).contour
|
||||
val cs = c.splitAt(listOf(1.0/3.0, 2.0/3.0))
|
||||
extend {
|
||||
drawer.strokeWeight = 5.0
|
||||
|
||||
drawer.stroke = ColorRGBa.PINK
|
||||
drawer.contour(cs[0])
|
||||
drawer.stroke = ColorRGBa.MEDIUM_PURPLE
|
||||
drawer.contour(cs[1])
|
||||
drawer.stroke = ColorRGBa.RED
|
||||
drawer.contour(cs[2])
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user