diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt new file mode 100644 index 000000000..1aba47552 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt @@ -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) { + var newVertexIndices = vertexIndices + var newEdgeIndices = edgeIndices + + for (adjustment in adjustments) { + when (adjustment) { + is SegmentOperation.Insert -> { + fun insert(list: List) = 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 +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt new file mode 100644 index 000000000..db80a6a4b --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt @@ -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 + } +} diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterExtensions.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterExtensions.kt new file mode 100644 index 000000000..086d4659e --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterExtensions.kt @@ -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() + } +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterVertex.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterVertex.kt new file mode 100644 index 000000000..f7de6494d --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterVertex.kt @@ -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) } + +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt new file mode 100644 index 000000000..92e293004 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt @@ -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 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] + */ +data class ContourEdge( + val contour: ShapeContour, + val segmentIndex: Int, + val adjustments: List = 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(), + 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) + } +} + diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt new file mode 100644 index 000000000..77eaa1fe0 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt @@ -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 = 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) + } +} + diff --git a/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt b/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt new file mode 100644 index 000000000..764bfb4aa --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt @@ -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>, + val operations: List +) { + + companion object { + val EMPTY = SegmentAdjustments(emptyList(), emptyList()) + } +} + +class SegmentAdjuster(val list: MutableList) { + val adjustments = mutableListOf() + + 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.adjust(block: SegmentAdjuster.() -> Unit) : List { + val adjuster = SegmentAdjuster(this) + adjuster.block() + return adjuster.adjustments +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/utilities/FromContours.kt b/orx-shapes/src/commonMain/kotlin/utilities/FromContours.kt new file mode 100644 index 000000000..b73063603 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/utilities/FromContours.kt @@ -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, 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() + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/utilities/InsertPoint.kt b/orx-shapes/src/commonMain/kotlin/utilities/InsertPoint.kt new file mode 100644 index 000000000..db81dd630 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/utilities/InsertPoint.kt @@ -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, weldEpsilon:Double = 1E-6) : ShapeContour { + val splitContours = splitAt(ascendingTs, weldEpsilon) + return ShapeContour.fromContours(splitContours, closed) +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/utilities/SplitAt.kt b/orx-shapes/src/commonMain/kotlin/utilities/SplitAt.kt new file mode 100644 index 000000000..9f291ebd9 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/utilities/SplitAt.kt @@ -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 { + val t = (1.0 / segments.size) * (segmentIndex + segmentT) + return splitAt(listOf(t)) +} + +fun ShapeContour.splitAt(ascendingTs: List, weldEpsilon: Double = 1E-6): List { + 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, weldEpsilon: Double = 1E-6): List { + 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]) + } +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/utilities/WeldAscending.kt b/orx-shapes/src/commonMain/kotlin/utilities/WeldAscending.kt new file mode 100644 index 000000000..ee82e00b3 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/utilities/WeldAscending.kt @@ -0,0 +1,21 @@ +package org.openrndr.extra.shapes.utilities + +/** + * Weld values if their distance is less than [epsilon] + */ +fun List.weldAscending(epsilon: Double = 1E-6): List { + 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 + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour01.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour01.kt new file mode 100644 index 000000000..9034a90a8 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour01.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour02.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour02.kt new file mode 100644 index 000000000..0ba783cb1 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour02.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour03.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour03.kt new file mode 100644 index 000000000..842394a2b --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour03.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour04.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour04.kt new file mode 100644 index 000000000..1cd4fef94 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour04.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour05.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour05.kt new file mode 100644 index 000000000..2b0135fa8 --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour05.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoSplit01.kt b/orx-shapes/src/jvmDemo/kotlin/DemoSplit01.kt new file mode 100644 index 000000000..c7022290f --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoSplit01.kt @@ -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]) + + } + } +}