[orx-shapes] Improve support for open contours in adjustContour

This commit is contained in:
Edwin Jakobs
2023-10-30 07:53:40 +01:00
parent 2293d5d74e
commit 6e14b36540
8 changed files with 132 additions and 59 deletions

View File

@@ -184,8 +184,8 @@ class ContourAdjuster(var contour: ShapeContour) {
* select multiple edges using an index-edge based [predicate] * select multiple edges using an index-edge based [predicate]
*/ */
fun selectEdges(predicate: (Int, ContourEdge) -> Boolean) { fun selectEdges(predicate: (Int, ContourEdge) -> Boolean) {
vertexSelection = edgeSelection =
(0 until if (contour.closed) contour.segments.size else contour.segments.size + 1).filter { index -> (contour.segments.indices).filter { index ->
predicate(index, ContourEdge(contour, index)) predicate(index, ContourEdge(contour, index))
} }
} }

View File

@@ -19,6 +19,11 @@ data class ContourAdjusterEdge(val contourAdjuster: ContourAdjuster, val segment
return contourAdjuster.contour.segments[segmentIndex()].normal(t) return contourAdjuster.contour.segments[segmentIndex()].normal(t)
} }
val length: Double
get() {
return contourAdjuster.contour.segments[segmentIndex()].length
}
/** /**
* A [ContourAdjusterVertex] interface for the start-vertex of the edge * A [ContourAdjusterVertex] interface for the start-vertex of the edge

View File

@@ -10,10 +10,34 @@ class ContourAdjusterVertex(val contourAdjuster: ContourAdjuster, val segmentInd
contourAdjuster.updateSelection(newVertex.adjustments) contourAdjuster.updateSelection(newVertex.adjustments)
} }
val position: Vector2 val previous: ContourAdjusterVertex?
get() { get() {
return contourAdjuster.contour.segments[segmentIndex()].start return if (contourAdjuster.contour.closed || segmentIndex() > 0) {
ContourAdjusterVertex(contourAdjuster, { (segmentIndex() - 1).mod(contourAdjuster.contour.segments.size) })
} else {
null
}
} }
val next: ContourAdjusterVertex?
get() {
return if (contourAdjuster.contour.closed || segmentIndex() < contourAdjuster.contour.segments.size-1) {
ContourAdjusterVertex(contourAdjuster, { (segmentIndex() + 1).mod(contourAdjuster.contour.segments.size) })
} else {
null
}
}
val t: Double
get() = ContourVertex(contourAdjuster.contour, segmentIndex(), emptyList()).t
val position: Vector2
get() = ContourVertex(contourAdjuster.contour, segmentIndex(), emptyList()).position
val normal: Vector2
get() = ContourVertex(contourAdjuster.contour, segmentIndex(), emptyList()).normal
fun select() { fun select() {
contourAdjuster.selectVertex(segmentIndex()) contourAdjuster.selectVertex(segmentIndex())

View File

@@ -85,6 +85,11 @@ data class ContourEdge(
} }
} }
val length: Double
get() {
return contour.segments[segmentIndex].length
}
/** /**
* replace this edge with a point at [t] * replace this edge with a point at [t]
@@ -124,7 +129,8 @@ data class ContourEdge(
}.windowed(2, 1).map { }.windowed(2, 1).map {
r.sub(it[0], it[1]) r.sub(it[0], it[1])
} }
return replacedWith(ShapeContour.fromContours(newSegments, false)) require(newSegments.size == parts)
return replacedWith(ShapeContour.fromContours(newSegments, false, 1.0))
} }
fun replacedWith(openContour: ShapeContour): ContourEdge { fun replacedWith(openContour: ShapeContour): ContourEdge {

View File

@@ -6,8 +6,12 @@ import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val adjustments: List<SegmentOperation> = emptyList()) { data class ContourVertex(
fun withoutAdjustments() : ContourVertex { val contour: ShapeContour,
val segmentIndex: Int,
val adjustments: List<SegmentOperation> = emptyList()
) {
fun withoutAdjustments(): ContourVertex {
return if (adjustments.isEmpty()) { return if (adjustments.isEmpty()) {
this this
} else { } else {
@@ -15,17 +19,40 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a
} }
} }
val position: Vector2 val normal: Vector2
get() { get() {
return contour.segments[segmentIndex].start return if (contour.closed || segmentIndex > 0 || segmentIndex < contour.segments.size) {
val segmentIn = contour.segments[(segmentIndex - 1).mod(contour.segments.size)]
val normalIn = segmentIn.normal(1.0)
val normalOut = contour.segments[segmentIndex].normal(0.0)
(normalIn + normalOut).normalized
} else if (segmentIndex == 0) {
contour.normal(0.0)
} else if (segmentIndex == contour.segments.size) {
contour.normal(1.0)
} else {
error("segmentIndex out of bounds ${segmentIndex} >= ${contour.segments.size}")
}
} }
fun remove(updateTangents: Boolean = true) : ContourVertex { val t: Double
get() = segmentIndex.toDouble() / contour.segments.size
val position: Vector2
get() {
return if (contour.closed || segmentIndex < contour.segments.size) {
contour.segments[segmentIndex].start
} else {
contour.segments[segmentIndex - 1].end
}
}
fun remove(updateTangents: Boolean = true): ContourVertex {
if (contour.empty) { if (contour.empty) {
return withoutAdjustments() return withoutAdjustments()
} }
val segmentInIndex = if (contour.closed) (segmentIndex-1).mod(contour.segments.size) else segmentIndex-1 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 segmentOutIndex = if (contour.closed) (segmentIndex + 1).mod(contour.segments.size) else segmentIndex + 1
val newSegments = contour.segments.map { it }.toMutableList() val newSegments = contour.segments.map { it }.toMutableList()
val refIn = newSegments.getOrNull(segmentInIndex) val refIn = newSegments.getOrNull(segmentInIndex)
val refOut = newSegments.getOrNull(segmentOutIndex) val refOut = newSegments.getOrNull(segmentOutIndex)
@@ -58,7 +85,7 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a
} }
val transform = buildTransform { val transform = buildTransform {
translate(position) translate(position)
this.rotate(rotationInDegrees) this@buildTransform.rotate(rotationInDegrees)
translate(-position) translate(-position)
} }
return transformTangents(transform, transform) return transformTangents(transform, transform)
@@ -94,7 +121,7 @@ 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) transformedBy(buildTransform { translate(translation) }, updateTangents)
fun rotatedBy(rotationInDegrees: Double, anchor:Vector2, updateTangents:Boolean=true): ContourVertex { fun rotatedBy(rotationInDegrees: Double, anchor: Vector2, updateTangents: Boolean = true): ContourVertex {
return transformedBy(buildTransform { return transformedBy(buildTransform {
translate(anchor) translate(anchor)
rotate(rotationInDegrees) rotate(rotationInDegrees)
@@ -102,7 +129,7 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a
}, updateTangents) }, updateTangents)
} }
fun scaledBy(scaleFactor: Double, anchor:Vector2, updateTangents:Boolean=true): ContourVertex { fun scaledBy(scaleFactor: Double, anchor: Vector2, updateTangents: Boolean = true): ContourVertex {
return transformedBy(buildTransform { return transformedBy(buildTransform {
translate(anchor) translate(anchor)
scale(scaleFactor) scale(scaleFactor)
@@ -110,36 +137,65 @@ data class ContourVertex(val contour: ShapeContour, val segmentIndex: Int, val a
}, updateTangents) }, updateTangents)
} }
fun transformedBy(transform: Matrix44, updateTangents: Boolean = true): ContourVertex { fun transformedBy(transform: Matrix44, updateTangents: Boolean): ContourVertex {
return transformedBy(updateTangents) { v -> v.transformedBy(transform) }
}
fun transformedBy(updateTangents: Boolean = true, transform: (Vector2) -> Vector2): ContourVertex {
if (contour.empty) { if (contour.empty) {
return withoutAdjustments() return withoutAdjustments()
} }
val newSegments = contour.segments.map { it }.toMutableList() val newSegments = contour.segments.map { it }.toMutableList()
val refOut = contour.segments[segmentIndex] val refOut = if (contour.closed || segmentIndex < contour.segments.size) {
val refIn = if (contour.closed) contour.segments[(segmentIndex - 1).mod(contour.segments.size)] else contour.segments[segmentIndex]
contour.segments.getOrNull(segmentIndex - 1)
val newPosition = refOut.start.transformedBy(transform)
newSegments[segmentIndex] = if (updateTangents && !refOut.linear) {
val cubicSegment = refOut.cubic
val newControls = arrayOf(cubicSegment.control[0].transformedBy(transform), cubicSegment.control[1])
refOut.copy(start = newPosition, control = newControls)
} else { } else {
newSegments[segmentIndex].copy(start = newPosition) contour.segments.last()
}
val refIn = if (contour.closed) {
contour.segments[(segmentIndex - 1).mod(contour.segments.size)]
} else {
contour.segments.getOrNull(segmentIndex - 1)
}
val newPosition = if (contour.closed || segmentIndex < contour.segments.size) {
transform(refOut.start)
} else {
transform(refOut.end)
}
if (contour.closed || segmentIndex< contour.segments.size) {
newSegments[segmentIndex] = if (updateTangents && !refOut.linear) {
val cubicSegment = refOut.cubic
val newControls = arrayOf(transform(cubicSegment.control[0]), cubicSegment.control[1])
refOut.copy(start = newPosition, control = newControls)
} else {
newSegments[segmentIndex].copy(start = newPosition)
}
} else {
newSegments[segmentIndex-1] = if (updateTangents && !refOut.linear) {
val cubicSegment = refOut.cubic
val newControls = arrayOf(cubicSegment.control[0], transform(cubicSegment.control[1]))
refOut.copy(end = newPosition, control = newControls)
} else {
newSegments[segmentIndex-1].copy(end = newPosition)
}
} }
val segmentIndexIn = (segmentIndex - 1).mod(contour.segments.size) val segmentIndexIn = (segmentIndex - 1).mod(contour.segments.size)
if (refIn != null) { if (refIn != null && (contour.closed || segmentIndex < contour.segments.size)) {
newSegments[segmentIndexIn] = newSegments[segmentIndexIn] =
if (updateTangents && !refIn.linear) { if (updateTangents && !refIn.linear) {
val cubicSegment = refIn.cubic val cubicSegment = refIn.cubic
val newControls = arrayOf(cubicSegment.control[0], cubicSegment.control[1].transformedBy(transform)) val newControls = arrayOf(cubicSegment.control[0], transform(cubicSegment.control[1]))
newSegments[segmentIndexIn].copy(control = newControls, end = newPosition) newSegments[segmentIndexIn].copy(control = newControls, end = newPosition)
} else { } else {
newSegments[segmentIndexIn].copy(end = newPosition) newSegments[segmentIndexIn].copy(end = newPosition)
} }
} }
for (s in newSegments.windowed(2, 1)) {
require(s[0].end.distanceTo(s[1].start) < 1E-3)
}
val newContour = ShapeContour.fromSegments(newSegments, contour.closed, contour.polarity) val newContour = ShapeContour.fromSegments(newSegments, contour.closed, contour.polarity)
return ContourVertex(newContour, segmentIndex) return ContourVertex(newContour, segmentIndex)

View File

@@ -16,7 +16,8 @@ fun ShapeContour.Companion.fromContours(contours: List<ShapeContour>, closed: Bo
for (c in contours.windowed(2,1,true)) { for (c in contours.windowed(2,1,true)) {
copy(c[0]) copy(c[0])
if (c.size == 2) { if (c.size == 2) {
if (c[0].position(1.0).distanceTo(c[1].position(0.0)) > connectEpsilon ) { val d = c[0].position(1.0).distanceTo(c[1].position(0.0))
if (d > connectEpsilon ) {
lineTo(c[1].position(0.0)) lineTo(c[1].position(0.0))
} }
} }

View File

@@ -20,7 +20,6 @@ fun main() {
parameters.selectInsertedVertices = true parameters.selectInsertedVertices = true
for (i in 0 until 4) { for (i in 0 until 4) {
val splitT = cos(seconds + i * Math.PI*0.5)*0.2+0.5 val splitT = cos(seconds + i * Math.PI*0.5)*0.2+0.5
selectEdges { it -> true } selectEdges { it -> true }

View File

@@ -2,10 +2,8 @@ import org.openrndr.application
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.extra.shapes.adjust.adjustContour import org.openrndr.extra.shapes.adjust.adjustContour
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
import org.openrndr.shape.contour import org.openrndr.shape.contour
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin
fun main() { fun main() {
application { application {
@@ -15,38 +13,22 @@ fun main() {
} }
program { program {
extend { extend {
var contour = var contour = contour {
Circle(drawer.bounds.center, 300.0).contour moveTo(drawer.bounds.center - Vector2(300.0, 0.0))
lineTo(drawer.bounds.center + Vector2(300.0, 0.0))
}
contour = adjustContour(contour) { contour = adjustContour(contour) {
selectEdges(0, 2) selectEdge(0)
edge.splitIn(128)
val tr = cos(seconds) * 0.5 + 0.5
for (e in edges) { selectVertices { i, v -> v.t >= tr }
e.replaceWith(contour { val anchor = contour.position(tr)
moveTo(e.startPosition)
lineTo(e.position(0.5) + e.normal(0.5) * cos(seconds) * 150.0)
lineTo(e.endPosition)
})
}
selectEdges(0, 1)
for (e in edges) { for (v in vertices) {
e.replaceWith(contour { v.rotate((v.t - tr) * 2000.0, anchor)
moveTo(e.startPosition) v.scale(0.05, anchor)
val t = 0.5
lineTo(e.position(t) + e.normal(t) * cos(seconds) * 50.0)
lineTo(e.endPosition)
})
}
selectEdges(0, 1)
for (e in edges) {
e.replaceWith(contour {
moveTo(e.startPosition)
val t = 0.5
lineTo(e.position(t) + e.normal(t) * sin(seconds) * 50.0)
lineTo(e.endPosition)
})
} }
} }