From 598761179dab08eec28d1f69e386caf4d948057d Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Fri, 27 Oct 2023 07:58:21 +0200 Subject: [PATCH] [orx-shapes] Add selection stacks and selection parameters --- .../kotlin/adjust/ContourAdjuster.kt | 134 ++++++++++++++---- .../kotlin/adjust/ContourAdjusterEdge.kt | 2 + .../kotlin/adjust/ContourAdjusterVertex.kt | 3 + .../commonMain/kotlin/adjust/ContourEdge.kt | 54 ++++++- .../commonMain/kotlin/adjust/ContourVertex.kt | 32 +++-- .../kotlin/adjust/SegmentAdjustments.kt | 4 +- .../src/jvmDemo/kotlin/DemoAdjustContour06.kt | 42 ++++++ 7 files changed, 232 insertions(+), 39 deletions(-) create mode 100644 orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour06.kt diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt index 034ef79a7..10d74cde5 100644 --- a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjuster.kt @@ -1,23 +1,43 @@ package org.openrndr.extra.shapes.adjust +import org.openrndr.collections.pop import org.openrndr.extra.shapes.vertex.ContourVertex import org.openrndr.shape.ShapeContour + /** * Adjusts [ShapeContour] using an accessible interface. * * [ContourAdjuster] */ class ContourAdjuster(var contour: ShapeContour) { + data class Parameters( + var selectInsertedEdges: Boolean = false, + var selectInsertedVertices: Boolean = false, + var clearSelectedEdges: Boolean = false, + var clearSelectedVertices: Boolean = false + ) + var parameters = Parameters() + + val parameterStack = ArrayDeque() + + fun pushParameters() { + parameterStack.addLast(parameters.copy()) + } + + fun popParameters() { + parameters = parameterStack.pop() + } + /** * selected vertex indices */ - var vertexIndices = listOf(0) + var vertexSelection = listOf(0) /** * selected edge indices */ - var edgeIndices = listOf(0) + var edgeSelection = listOf(0) private var vertexWorkingSet = emptyList() private var edgeWorkingSet = emptyList() @@ -25,17 +45,48 @@ class ContourAdjuster(var contour: ShapeContour) { private var vertexHead = emptyList() private var edgeHead = emptyList() + + private val vertexSelectionStack = ArrayDeque>() + private val edgeSelectionStack = ArrayDeque>() + + fun pushVertexSelection() { + vertexSelectionStack.addLast(vertexSelection) + } + + fun popVertexSelection() { + vertexSelection = vertexSelectionStack.removeLast() + } + + fun pushEdgeSelection() { + edgeSelectionStack.addLast(edgeSelection) + } + + fun popEdgeSelection() { + edgeSelection = edgeSelectionStack.removeLast() + } + + fun pushSelection() { + pushEdgeSelection() + pushVertexSelection() + } + + fun popSelection() { + popEdgeSelection() + popVertexSelection() + } + /** * the selected vertex */ val vertex: ContourAdjusterVertex get() { - return ContourAdjusterVertex(this, { vertexIndices.first() } ) + return vertices.first() } val vertices: Sequence get() { - vertexWorkingSet = vertexIndices + vertexWorkingSet = vertexSelection + applyBeforeAdjustment() return sequence { while (vertexWorkingSet.isNotEmpty()) { vertexHead = vertexWorkingSet.take(1) @@ -51,12 +102,14 @@ class ContourAdjuster(var contour: ShapeContour) { */ val edge: ContourAdjusterEdge get() { - return ContourAdjusterEdge(this, { edgeIndices.first() }) + return edges.first() } val edges: Sequence get() { - edgeWorkingSet = edgeIndices + edgeWorkingSet = edgeSelection + applyBeforeAdjustment() + return sequence { while (edgeWorkingSet.isNotEmpty()) { edgeHead = edgeWorkingSet.take(1) @@ -70,28 +123,28 @@ class ContourAdjuster(var contour: ShapeContour) { * select a vertex by index */ fun selectVertex(index: Int) { - vertexIndices = listOf(index) + vertexSelection = listOf(index) } /** * deselect a vertex by index */ fun deselectVertex(index: Int) { - vertexIndices = vertexIndices.filter { it != index } + vertexSelection = vertexSelection.filter { it != index } } /** * select multiple vertices */ fun selectVertices(vararg indices: Int) { - vertexIndices = indices.toList().distinct() + vertexSelection = indices.toList().distinct() } /** * select multiple vertices using an index based [predicate] */ fun selectVertices(predicate: (Int) -> Boolean) { - vertexIndices = + vertexSelection = (0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter(predicate) } @@ -99,7 +152,7 @@ class ContourAdjuster(var contour: ShapeContour) { * select multiple vertices using an index-vertex based [predicate] */ fun selectVertices(predicate: (Int, ContourVertex) -> Boolean) { - vertexIndices = + vertexSelection = (0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter { index -> predicate(index, ContourVertex(contour, index)) } @@ -116,14 +169,14 @@ class ContourAdjuster(var contour: ShapeContour) { * select multiple edges by index */ fun selectEdges(vararg indices: Int) { - edgeIndices = indices.toList().distinct() + edgeSelection = indices.toList().distinct() } /** * select multiple vertices using an index based [predicate] */ fun selectEdges(predicate: (Int) -> Boolean) { - edgeIndices = + edgeSelection = contour.segments.indices.filter(predicate) } @@ -131,32 +184,57 @@ class ContourAdjuster(var contour: ShapeContour) { * select multiple edges using an index-edge based [predicate] */ fun selectEdges(predicate: (Int, ContourEdge) -> Boolean) { - vertexIndices = + vertexSelection = (0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter { index -> predicate(index, ContourEdge(contour, index)) } } - fun updateSelection(adjustments: List) { + private fun applyBeforeAdjustment() { + if (parameters.clearSelectedEdges) { + edgeSelection = emptyList() + } + if (parameters.clearSelectedVertices) { + vertexSelection = emptyList() + } + } + fun updateSelection(adjustments: List) { for (adjustment in adjustments) { when (adjustment) { is SegmentOperation.Insert -> { - fun insert(list: List) = list.map { - if (it >= adjustment.index) { - it + adjustment.amount + fun insert(list: List, selectInserted: Boolean = false) = + if (!selectInserted) { + list.map { + if (it >= adjustment.index) { + it + adjustment.amount + } else { + it + } + } } else { - it + (list.flatMap { + if (it >= adjustment.index) { + listOf(it + adjustment.amount) + (it + 1..it + adjustment.amount) + } else { + listOf(it) + } + } + (adjustment.index.. { - fun remove(list: List) = list.mapNotNull { if (it in adjustment.index.. List.update(vararg updates: Pair): List { if (updates.isEmpty()) { return this @@ -24,12 +24,20 @@ fun List.update(vararg updates: Pair): List { * 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 = emptyList() ) { + /** + * provide a copy without the list of adjustments + */ fun withoutAdjustments(): ContourEdge { return if (adjustments.isEmpty()) { this @@ -38,9 +46,26 @@ data class ContourEdge( } } + /** + * convert the edge to a linear edge, truncating control points if those exist + */ + fun toLinear(): ContourEdge { + if (contour.segments[segmentIndex].type != SegmentType.LINEAR) { + val newSegment = contour.segments[segmentIndex].copy(control = emptyArray()) + val newSegments = contour.segments + .update(segmentIndex to newSegment) + + return ContourEdge( + ShapeContour.fromSegments(newSegments, contour.closed), + segmentIndex + ) + } else { + return this + } + } /** - * + * convert the edge to a cubic edge */ fun toCubic(): ContourEdge { if (contour.segments[segmentIndex].type != SegmentType.CUBIC) { @@ -57,8 +82,10 @@ data class ContourEdge( } } + /** - * + * 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) { @@ -83,11 +110,16 @@ data class ContourEdge( 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) { + 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 = @@ -109,6 +141,10 @@ data class ContourEdge( } } + /** + * 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() @@ -121,6 +157,11 @@ data class ContourEdge( } } + + /** + * 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( @@ -133,7 +174,6 @@ data class ContourEdge( val refIn = contour.segments.getOrNull(segmentInIndex) val refOut = contour.segments.getOrNull(segmentOutIndex) - val newSegments = contour.segments.map { it }.toMutableList() if (refIn != null) { @@ -176,6 +216,10 @@ data class ContourEdge( 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) diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt index 77eaa1fe0..3c87a2974 100644 --- a/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourVertex.kt @@ -1,9 +1,6 @@ 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.extra.shapes.adjust.* import org.openrndr.math.Matrix44 import org.openrndr.math.Vector2 import org.openrndr.math.transforms.buildTransform @@ -94,7 +91,26 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a } - fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourVertex { + fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourVertex = + transformedBy(buildTransform { translate(translation) }, updateTangents) + + fun rotatedBy(rotationInDegrees: Double, anchor:Vector2, updateTangents:Boolean=true): ContourVertex { + return transformedBy(buildTransform { + translate(anchor) + rotate(rotationInDegrees) + translate(-anchor) + }, updateTangents) + } + + fun scaledBy(scaleFactor: Double, anchor:Vector2, updateTangents:Boolean=true): ContourVertex { + return transformedBy(buildTransform { + translate(anchor) + scale(scaleFactor) + translate(-anchor) + }, updateTangents) + } + + fun transformedBy(transform: Matrix44, updateTangents: Boolean = true): ContourVertex { if (contour.empty) { return withoutAdjustments() } @@ -103,10 +119,10 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a 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 + val newPosition = refOut.start.transformedBy(transform) newSegments[segmentIndex] = if (updateTangents && !refOut.linear) { val cubicSegment = refOut.cubic - val newControls = arrayOf(cubicSegment.control[0] + translation, cubicSegment.control[1]) + val newControls = arrayOf(cubicSegment.control[0].transformedBy(transform), cubicSegment.control[1]) refOut.copy(start = newPosition, control = newControls) } else { newSegments[segmentIndex].copy(start = newPosition) @@ -117,7 +133,7 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a newSegments[segmentIndexIn] = if (updateTangents && !refIn.linear) { val cubicSegment = refIn.cubic - val newControls = arrayOf(cubicSegment.control[0], cubicSegment.control[1] + translation) + val newControls = arrayOf(cubicSegment.control[0], cubicSegment.control[1].transformedBy(transform)) newSegments[segmentIndexIn].copy(control = newControls, end = newPosition) } else { diff --git a/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt b/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt index 764bfb4aa..af3f74d45 100644 --- a/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt +++ b/orx-shapes/src/commonMain/kotlin/adjust/SegmentAdjustments.kt @@ -3,8 +3,8 @@ 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 + data class Remove(val index: Int, val amount: Int) : SegmentOperation + data class Insert(val index: Int, val amount: Int) : SegmentOperation } diff --git a/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour06.kt b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour06.kt new file mode 100644 index 000000000..cc1b9dc6a --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/DemoAdjustContour06.kt @@ -0,0 +1,42 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.shapes.adjust.adjustContour +import org.openrndr.shape.Circle +import kotlin.math.cos + +fun main() { + application { + configure { + width = 800 + height = 800 + } + program { + extend { + var contour = + Circle(drawer.bounds.center, 300.0).contour + + contour = adjustContour(contour) { + parameters.clearSelectedVertices = true + parameters.selectInsertedVertices = true + + + + for (i in 0 until 4) { + val splitT = cos(seconds + i * Math.PI*0.5)*0.2+0.5 + selectEdges { it -> true } + for (e in edges) { + e.splitAt(splitT) + } + // as a resut of the clearSelectedVertices and selectInsertedVertices settings + // the vertex selection is set to the newly inserted vertices + for ((index, v) in vertices.withIndex()) { + v.scale(cos(seconds + i + index) * 0.5 * (1.0 / (1.0 + i)) + 1.0, drawer.bounds.center) + } + } + } + drawer.stroke = ColorRGBa.RED + drawer.contour(contour) + } + } + } +} \ No newline at end of file