Update for OPENRNDR segment and path generalizations

This commit is contained in:
Edwin Jakobs
2024-03-19 16:31:45 +01:00
parent 8fe7631570
commit af6d35c59b
37 changed files with 579 additions and 277 deletions

View File

@@ -570,28 +570,28 @@ class CompositionDrawer(documentBounds: CompositionDimensions = defaultCompositi
c1: Vector2,
end: Vector2,
insert: Boolean = true
) = segment(Segment(start, c0, c1, end), insert)
) = segment(Segment2D(start, c0, c1, end), insert)
fun segment(
start: Vector2,
c0: Vector2,
end: Vector2,
insert: Boolean = true
) = segment(Segment(start, c0, end), insert)
) = segment(Segment2D(start, c0, end), insert)
fun segment(
start: Vector2,
end: Vector2,
insert: Boolean = true
) = segment(Segment(start, end), insert)
) = segment(Segment2D(start, end), insert)
fun segment(
segment: Segment,
segment: Segment2D,
insert: Boolean = true
) = contour(segment.contour, insert)
fun segments(
segments: List<Segment>,
segments: List<Segment2D>,
insert: Boolean = true
) = segments.map {
segment(it, insert)

View File

@@ -3,14 +3,14 @@ package org.openrndr.extra.fcurve
import kotlinx.serialization.Serializable
import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
import kotlin.math.abs
/**
* Find the (first) t value for a given [x] value
*/
private fun Segment.tForX(x: Double): Double {
private fun Segment2D.tForX(x: Double): Double {
if (linear) {
return (x - start.x) / (end.x - start.x)
} else {
@@ -29,7 +29,7 @@ private fun Segment.tForX(x: Double): Double {
/**
* Find the y value for a given [x] value
*/
private fun Segment.yForX(x: Double): Double {
private fun Segment2D.yForX(x: Double): Double {
val t = tForX(x)
return position(t).y
}
@@ -37,7 +37,7 @@ private fun Segment.yForX(x: Double): Double {
/**
* Scale tangents such that tangent lines do not overlap
*/
fun Segment.scaleTangents(axis: Vector2 = Vector2.UNIT_X): Segment {
fun Segment2D.scaleTangents(axis: Vector2 = Vector2.UNIT_X): Segment2D {
if (linear) {
return this
} else {
@@ -74,7 +74,7 @@ fun Segment.scaleTangents(axis: Vector2 = Vector2.UNIT_X): Segment {
* Fcurve class
*/
@Serializable
data class FCurve(val segments: List<Segment>) {
data class FCurve(val segments: List<Segment2D>) {
/**
* Reverse the fcurve
@@ -105,7 +105,7 @@ data class FCurve(val segments: List<Segment>) {
* Create a sampler or function from the Fcurve
*/
fun sampler(normalized: Boolean = false): (Double) -> Double {
var cachedSegment: Segment? = null
var cachedSegment: Segment2D? = null
if (!normalized) {
return { t ->
val r = valueWithSegment(t, cachedSegment)
@@ -139,13 +139,13 @@ data class FCurve(val segments: List<Segment>) {
* @param segment an optional segment that can be used to speed up scanning for the relevant segment
* @see valueWithSegment
*/
fun value(t: Double, segment: Segment? = null): Double = valueWithSegment(t, segment).first
fun value(t: Double, segment: Segment2D? = null): Double = valueWithSegment(t, segment).first
/**
* Evaluate the Fcurve at [t]
* @param segment an optional segment that can be used to speed up scanning for the relevant segment
*/
fun valueWithSegment(t: Double, cachedSegment: Segment? = null): Pair<Double, Segment?> {
fun valueWithSegment(t: Double, cachedSegment: Segment2D? = null): Pair<Double, Segment2D?> {
if (cachedSegment != null) {
if (t >= cachedSegment.start.x && t < cachedSegment.end.x) {
return Pair(cachedSegment.yForX(t), cachedSegment)
@@ -180,7 +180,7 @@ data class FCurve(val segments: List<Segment>) {
* Return a list of contours that can be used to visualize the Fcurve
*/
fun contours(scale: Vector2 = Vector2.ONE): List<ShapeContour> {
var active = mutableListOf<Segment>()
var active = mutableListOf<Segment2D>()
val result = mutableListOf<ShapeContour>()
for (segment in segments) {
@@ -210,7 +210,7 @@ data class FCurve(val segments: List<Segment>) {
* Fcurve builder
*/
class FCurveBuilder {
val segments = mutableListOf<Segment>()
val segments = mutableListOf<Segment2D>()
var cursor = Vector2(0.0, 0.0)
var path = ""
@@ -222,7 +222,7 @@ class FCurveBuilder {
fun lineTo(x: Double, y: Double, relative: Boolean = false) {
val r = if (relative) 1.0 else 0.0
segments.add(Segment(cursor, Vector2(x + cursor.x, y + cursor.y * r)))
segments.add(Segment2D(cursor, Vector2(x + cursor.x, y + cursor.y * r)))
cursor = Vector2(cursor.x + x, cursor.y * r + y)
path += "${if (relative) "l" else "L"}$x,$y"
}
@@ -234,7 +234,7 @@ class FCurveBuilder {
) {
val r = if (relative) 1.0 else 0.0
segments.add(
Segment(
Segment2D(
cursor,
Vector2(cursor.x + x0, cursor.y * r + y0),
Vector2(cursor.x + x, cursor.y * r + y)
@@ -251,7 +251,7 @@ class FCurveBuilder {
) {
val r = if (relative) 1.0 else 0.0
segments.add(
Segment(
Segment2D(
cursor,
Vector2(cursor.x + x0, cursor.y * r + y0),
Vector2(cursor.x + x1, cursor.y * r + y1),
@@ -272,7 +272,7 @@ class FCurveBuilder {
val dy = outPos.y - outTangent.y
val ts = x / lastDuration
segments.add(
Segment(
Segment2D(
cursor,
Vector2(cursor.x + dx * ts, cursor.y + dy),
Vector2(cursor.x + x * 0.66, cursor.y * r + y),
@@ -290,7 +290,7 @@ class FCurveBuilder {
val dx = cursor.x - outTangent.x
val dy = cursor.y - outTangent.y
segments.add(
Segment(
Segment2D(
cursor,
Vector2(cursor.x + dx, cursor.y + dy),
Vector2(cursor.x + x1, cursor.y * r + y1),
@@ -370,7 +370,7 @@ private fun evaluateFCurveCommands(parts: List<String>): FCurve {
*/
return fcurve {
fun dx(): Double {
val lastSegment = segments.lastOrNull() ?: Segment(Vector2.ZERO, Vector2.ZERO)
val lastSegment = segments.lastOrNull() ?: Segment2D(Vector2.ZERO, Vector2.ZERO)
return lastSegment.end.x - lastSegment.start.x
}

View File

@@ -1,26 +1,26 @@
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.extra.runway.*
/**
* This example requires a `runway/BASNet` model to be active in Runway.
*/
fun main() = application {
configure {
width = 331
height = 400
}
program {
val image = loadImage("demo-data/images/life-cover.jpg")
val result: BASNETResult =
runwayQuery("http://localhost:8000/query", BASNETRequest(image.toData()))
val segmentImage = ColorBuffer.fromData(result.image)
extend {
drawer.image(segmentImage, 0.0, 0.0)
}
}
}
//import org.openrndr.application
//import org.openrndr.draw.*
//import org.openrndr.extra.runway.*
//
///**
// * This example requires a `runway/BASNet` model to be active in Runway.
// */
//fun main() = application {
// configure {
// width = 331
// height = 400
// }
//
// program {
// val image = loadImage("demo-data/images/life-cover.jpg")
//
// val result: BASNETResult =
// runwayQuery("http://localhost:8000/query", BASNETRequest(image.toData()))
//
// val segmentImage = ColorBuffer.fromData(result.image)
//
// extend {
// drawer.image(segmentImage, 0.0, 0.0)
// }
// }
//}

View File

@@ -86,7 +86,7 @@ fun ShapeProvider.scatter(
hg
}
fun Segment.randomPoints(count: Int) = sequence {
fun Segment2D.randomPoints(count: Int) = sequence {
for (i in 0 until count) {
val t = random.nextDouble()
yield(position(t) - normal(t).normalized * distanceToEdge)

View File

@@ -37,6 +37,7 @@ kotlin {
implementation(project(":orx-triangulation"))
implementation(project(":orx-shapes"))
implementation(project(":orx-noise"))
implementation(project(":orx-mesh-generators"))
}
}
}

View File

@@ -3,14 +3,14 @@ package org.openrndr.extra.shapes.adjust
import org.openrndr.collections.pop
import org.openrndr.extra.shapes.vertex.ContourVertex
import org.openrndr.math.Vector2
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
import kotlin.jvm.JvmName
class ContourAdjusterStatus(
val contour: ShapeContour,
val selectedSegments: List<Segment>,
val selectedSegments: List<Segment2D>,
val selectedPoints: List<Vector2>
)

View File

@@ -6,7 +6,7 @@ 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.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.SegmentType
import org.openrndr.shape.ShapeContour
import kotlin.math.abs
@@ -151,7 +151,7 @@ data class ContourEdge(
removeAt(segmentIndex)
if (segment.start.distanceTo(openContour.position(0.0)) > 1E-3) {
add(insertIndex, Segment(segment.start, openContour.position(0.0)))
add(insertIndex, Segment2D(segment.start, openContour.position(0.0)))
insertIndex++
}
for (s in openContour.segments) {
@@ -159,7 +159,7 @@ data class ContourEdge(
insertIndex++
}
if (segment.end.distanceTo(openContour.position(1.0)) > 1E-3) {
add(insertIndex, Segment(segment.end, openContour.position(1.0)))
add(insertIndex, Segment2D(segment.end, openContour.position(1.0)))
}
}
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)

View File

@@ -1,6 +1,6 @@
package org.openrndr.extra.shapes.adjust
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
sealed interface SegmentOperation {
data class Remove(val index: Int, val amount: Int) : SegmentOperation
@@ -9,7 +9,7 @@ sealed interface SegmentOperation {
class SegmentAdjustments(
val replacements: List<Triple<Int, Segment, Segment>>,
val replacements: List<Triple<Int, Segment2D, Segment2D>>,
val operations: List<SegmentOperation>
) {
@@ -18,24 +18,24 @@ class SegmentAdjustments(
}
}
class SegmentAdjuster(val list: MutableList<Segment>) {
class SegmentAdjuster(val list: MutableList<Segment2D>) {
val adjustments = mutableListOf<SegmentOperation>()
fun removeAt(index: Int) {
list.removeAt(index)
adjustments.add(SegmentOperation.Remove(index, 1))
}
fun add(segment: Segment) {
fun add(segment: Segment2D) {
list.add(segment)
adjustments.add(SegmentOperation.Insert(list.lastIndex, 1))
}
fun add(index: Int, segment: Segment) {
fun add(index: Int, segment: Segment2D) {
list.add(index, segment)
adjustments.add(SegmentOperation.Insert(index, 1))
}
}
fun MutableList<Segment>.adjust(block: SegmentAdjuster.() -> Unit) : List<SegmentOperation> {
fun MutableList<Segment2D>.adjust(block: SegmentAdjuster.() -> Unit) : List<SegmentOperation> {
val adjuster = SegmentAdjuster(this)
adjuster.block()
return adjuster.adjustments

View File

@@ -111,13 +111,13 @@ class AlphaShape(val points: List<Vector2>) {
private fun edgesToShapeContour(edges: List<Pair<Int, Int>>): ShapeContour {
if (edges.isEmpty()) return ShapeContour.EMPTY
val mapping = edges.toMap()
val segments = mutableListOf<Segment>()
val segments = mutableListOf<Segment2D>()
val start = edges.first().first
var current = start
val left = edges.map { it.first }.toMutableSet()
for (i in edges.indices) {
val next = mapping[current]!!
segments.add(Segment(getVec(current), getVec(next)))
segments.add(Segment2D(getVec(current), getVec(next)))
left.remove(current)
current = next
if (current == start) break
@@ -140,11 +140,11 @@ class AlphaShape(val points: List<Vector2>) {
while (left.isNotEmpty()) {
val start = left.first()
var current = start
val segments = mutableListOf<Segment>()
val segments = mutableListOf<Segment2D>()
val contourPoints = mutableListOf<Vector2>()
for (i in edges.indices) {
val next = mapping[current]!!
segments.add(Segment(getVec(current), getVec(next)))
segments.add(Segment2D(getVec(current), getVec(next)))
contourPoints.add(getVec(current))
left.remove(current)
current = next

View File

@@ -6,7 +6,7 @@ import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
import kotlin.random.Random
@@ -123,7 +123,7 @@ open class BezierPatchBase<C>(
cps[j] += points[i][j] * cs[i]
}
}
return ShapeContour(listOf(Segment(cps[0], cps[1], cps[2], cps[3])), false)
return ShapeContour(listOf(Segment2D(cps[0], cps[1], cps[2], cps[3])), false)
}
fun vertical(u: Double): ShapeContour {
@@ -134,33 +134,33 @@ open class BezierPatchBase<C>(
cps[j] += points[j][i] * cs[i]
}
}
return ShapeContour(listOf(Segment(cps[0], cps[1], cps[2], cps[3])), false)
return ShapeContour(listOf(Segment2D(cps[0], cps[1], cps[2], cps[3])), false)
}
/**
* Extract a sub-patch based on uv parameterization
*/
fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatchBase<C> {
val c0 = Segment(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1)
val c1 = Segment(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1)
val c2 = Segment(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1)
val c3 = Segment(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1)
val c0 = Segment2D(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1)
val c1 = Segment2D(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1)
val c2 = Segment2D(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1)
val c3 = Segment2D(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1)
val sub0 = bezierPatch(c0, c1, c2, c3)
val d0 = Segment(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1)
val d1 = Segment(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1)
val d2 = Segment(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1)
val d3 = Segment(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1)
val d0 = Segment2D(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1)
val d1 = Segment2D(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1)
val d2 = Segment2D(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1)
val d3 = Segment2D(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1)
return fromSegments<C>(d0, d1, d2, d3).transposed
}
val contour: ShapeContour = ShapeContour(
listOf(
Segment(points[0][0], points[0][1], points[0][2], points[0][3]),
Segment(points[0][3], points[1][3], points[2][3], points[3][3]),
Segment(points[3][3], points[3][2], points[3][1], points[3][0]),
Segment(points[3][0], points[2][0], points[1][0], points[0][0]),
Segment2D(points[0][0], points[0][1], points[0][2], points[0][3]),
Segment2D(points[0][3], points[1][3], points[2][3], points[3][3]),
Segment2D(points[3][3], points[3][2], points[3][1], points[3][0]),
Segment2D(points[3][0], points[2][0], points[1][0], points[0][0]),
), true
)
@@ -196,7 +196,7 @@ open class BezierPatchBase<C>(
}
companion object {
fun <C> fromSegments(c0: Segment, c1: Segment, c2: Segment, c3: Segment): BezierPatchBase<C>
fun <C> fromSegments(c0: Segment2D, c1: Segment2D, c2: Segment2D, c3: Segment2D): BezierPatchBase<C>
where C : AlgebraicColor<C>, C : ConvertibleToColorRGBa {
val c0c = c0.cubic
val c1c = c1.cubic
@@ -219,7 +219,7 @@ class BezierPatch(points: List<List<Vector2>>, colors: List<List<ColorRGBa>> = e
/**
* Create a cubic bezier patch from 4 segments. The control points of the segments are used in row-wise fashion
*/
fun bezierPatch(c0: Segment, c1: Segment, c2: Segment, c3: Segment): BezierPatch {
fun bezierPatch(c0: Segment2D, c1: Segment2D, c2: Segment2D, c3: Segment2D): BezierPatch {
val c0c = c0.cubic
val c1c = c1.cubic
val c2c = c2.cubic
@@ -289,7 +289,7 @@ fun BezierPatch.distort(shapeContour: ShapeContour, referenceRectangle: Rectangl
val ns = position(s.x, s.y)
val nc0 = position(c0.x, c0.y)
val nc1 = position(c1.x, c1.y)
Segment(ns, nc0, nc1, ne)
Segment2D(ns, nc0, nc1, ne)
}
return ShapeContour(distortedSegments, shapeContour.closed, shapeContour.polarity)
}

View File

@@ -140,16 +140,16 @@ open class BezierPatch3DBase<C>(
* Extract a sub-patch based on uv parameterization
*/
fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatch3DBase<C> {
val c0 = Segment3D(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1)
val c1 = Segment3D(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1)
val c2 = Segment3D(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1)
val c3 = Segment3D(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1)
val c0 = Segment3D(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1) as Segment3D
val c1 = Segment3D(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1) as Segment3D
val c2 = Segment3D(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1) as Segment3D
val c3 = Segment3D(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1) as Segment3D
val sub0 = bezierPatch(c0, c1, c2, c3)
val d0 = Segment3D(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1)
val d1 = Segment3D(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1)
val d2 = Segment3D(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1)
val d3 = Segment3D(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1)
val d0 = Segment3D(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1) as Segment3D
val d1 = Segment3D(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1) as Segment3D
val d2 = Segment3D(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1) as Segment3D
val d3 = Segment3D(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1) as Segment3D
return fromSegments<C>(d0, d1, d2, d3).transposed
}

View File

@@ -28,8 +28,8 @@ fun ContourBlend(a: ShapeContour, b: ShapeContour): ContourBlend {
val rb = b.rectified()
val sa = ra.splitForBlend(rb)
val sb = rb.splitForBlend(ra)
require(sa.contour.segments.size == sb.contour.segments.size) {
"preprocessing for contours failed to produce equal number of segments. ${sa.contour.segments.size}, ${sb.contour.segments.size}"
require(sa.path.segments.size == sb.path.segments.size) {
"preprocessing for contours failed to produce equal number of segments. ${sa.path.segments.size}, ${sb.path.segments.size}"
}
return ContourBlend(sa, sb)
}

View File

@@ -0,0 +1,37 @@
package org.openrndr.extra.shapes.blend
import org.openrndr.extra.shapes.rectify.RectifiedContour
import org.openrndr.extra.shapes.rectify.RectifiedPath3D
import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.shape.Path3D
import org.openrndr.shape.ShapeContour
/**
* ContourBlend holds two rectified contours with an equal amount of segments
*/
class Path3DBlend(val a: RectifiedPath3D, val b: RectifiedPath3D) {
fun mix(blendFunction: (Double) -> Double): Path3D {
return a.mix(b, blendFunction)
}
fun mix(blend: Double): Path3D {
return a.mix(b) { blend }
}
}
/**
* Create a [ContourBlend] for contours [a] and [b]
*
* Finding the pose that minimizes the error between [a] and [b] is not part of this function's work.
*
*/
fun Path3DBlend(a: Path3D, b: Path3D): Path3DBlend {
val ra = a.rectified()
val rb = b.rectified()
val sa = ra.splitForBlend(rb)
val sb = rb.splitForBlend(ra)
require(sa.path.segments.size == sb.path.segments.size) {
"preprocessing for contours failed to produce equal number of segments. ${sa.path.segments.size}, ${sb.path.segments.size}"
}
return Path3DBlend(sa, sb)
}

View File

@@ -3,28 +3,28 @@ package org.openrndr.extra.shapes.blend
import org.openrndr.extra.shapes.rectify.RectifiedContour
import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.extra.shapes.utilities.fromContours
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
/**
* Split for blending with [other]
*/
fun RectifiedContour.splitForBlend(other: RectifiedContour): RectifiedContour {
val ts = (0 until other.contour.segments.size + 1).map { it.toDouble() / other.contour.segments.size }
val ts = (0 until other.path.segments.size + 1).map { it.toDouble() / other.path.segments.size }
val rts = ts.map { other.inverseRectify(it) }
return ShapeContour.fromContours(splitAt(rts), contour.closed && other.contour.closed).rectified()
return ShapeContour.fromContours(splitAt(rts), path.closed && other.path.closed).rectified()
}
fun RectifiedContour.mix(other: RectifiedContour, blendFunction: (Double) -> Double): ShapeContour {
val n = this.contour.segments.size.toDouble()
val segs = (this.contour.segments zip other.contour.segments).mapIndexed { index, it ->
val n = this.path.segments.size.toDouble()
val segs = (this.path.segments zip other.path.segments).mapIndexed { index, it ->
val t0 = inverseRectify(index / n)
val t1 = inverseRectify((index + 1 / 3.0) / n)
val t2 = inverseRectify((index + 2 / 3.0) / n)
val t3 = inverseRectify((index + 1) / n)
it.first.mix(it.second, blendFunction(t0), blendFunction(t1), blendFunction(t2), blendFunction(t3))
(it.first as Segment2D).mix(it.second as Segment2D, blendFunction(t0), blendFunction(t1), blendFunction(t2), blendFunction(t3))
}
return ShapeContour.fromSegments(segs, contour.closed && other.contour.closed)
return ShapeContour.fromSegments(segs, path.closed && other.path.closed)
}

View File

@@ -0,0 +1,30 @@
package org.openrndr.extra.shapes.blend
import org.openrndr.extra.shapes.rectify.RectifiedPath3D
import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.extra.shapes.utilities.fromPaths
import org.openrndr.shape.Path3D
import org.openrndr.shape.Segment3D
/**
* Split for blending with [other]
*/
fun RectifiedPath3D.splitForBlend(other: RectifiedPath3D): RectifiedPath3D {
val ts = (0 until other.path.segments.size + 1).map { it.toDouble() / other.path.segments.size }
val rts = ts.map { other.inverseRectify(it) }
return Path3D.fromPaths(splitAt(rts), path.closed && other.path.closed).rectified()
}
fun RectifiedPath3D.mix(other: RectifiedPath3D, blendFunction: (Double) -> Double): Path3D {
val n = this.path.segments.size.toDouble()
val segs = (this.path.segments zip other.path.segments).mapIndexed { index, it ->
val t0 = inverseRectify(index / n)
val t1 = inverseRectify((index + 1 / 3.0) / n)
val t2 = inverseRectify((index + 2 / 3.0) / n)
val t3 = inverseRectify((index + 1) / n)
(it.first as Segment3D).mix(it.second as Segment3D, blendFunction(t0), blendFunction(t1), blendFunction(t2), blendFunction(t3))
}
return Path3D.fromSegments(segs, path.closed && other.path.closed)
}

View File

@@ -1,7 +1,9 @@
package org.openrndr.extra.shapes.blend
import org.openrndr.math.mix
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.Segment3D
/**
* Cubic segment mix
@@ -11,14 +13,14 @@ import org.openrndr.shape.Segment
* @param f2 the mix factor for the second control point
* @param f3 the mix factor for the end point
*/
fun Segment.mix(other: Segment, f0: Double, f1: Double, f2: Double, f3: Double): Segment {
fun Segment2D.mix(other: Segment2D, f0: Double, f1: Double, f2: Double, f3: Double): Segment2D {
val ac = this.cubic
val bc = other.cubic
val acc = if (ac.corner) 1.0 else 0.0
val bcc = if (bc.corner) 1.0 else 0.0
return Segment(
return Segment2D(
ac.start.mix(bc.start, f0),
ac.control[0].mix(bc.control[0], f1),
ac.control[1].mix(bc.control[1], f2),
@@ -26,3 +28,23 @@ fun Segment.mix(other: Segment, f0: Double, f1: Double, f2: Double, f3: Double):
corner = mix(acc, bcc, f0) >= 0.5
)
}
/**
* Cubic segment mix
* @param other the segment to mix with
* @param f0 the mix factor for the start point
* @param f1 the mix factor for the first control point
* @param f2 the mix factor for the second control point
* @param f3 the mix factor for the end point
*/
fun Segment3D.mix(other: Segment3D, f0: Double, f1: Double, f2: Double, f3: Double): Segment3D {
val ac = this.cubic
val bc = other.cubic
return Segment3D(
ac.start.mix(bc.start, f0),
ac.control[0].mix(bc.control[0], f1),
ac.control[1].mix(bc.control[1], f2),
ac.end.mix(bc.end, f3),
)
}

View File

@@ -26,7 +26,7 @@ val Iterable<Shape>.bounds : Rectangle
/**
* Evaluates the bounds around all [Segment] instances in the [Iterable]
*/
val Iterable<Segment>.bounds : Rectangle
val Iterable<Segment2D>.bounds : Rectangle
@JvmName("segmentBounds")
get() = map {
it.bounds

View File

@@ -2,7 +2,7 @@ package org.openrndr.extra.shapes.hobbycurve
// Code adapted from http://weitz.de/hobby/
import org.openrndr.math.Vector2
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.Shape
import org.openrndr.shape.ShapeContour
import kotlin.math.atan2
@@ -109,7 +109,7 @@ fun hobbyCurve(points: List<Vector2>, closed: Boolean = false, curl: Double = 0.
c2s.add(points[(i+1) % m] - v2 * rho(beta[i]!!, alpha[i]) * distances[i] / 3.0)
}
return ShapeContour(List(n) { Segment(points[it], c1s[it], c2s[it], points[(it+1)%m]) }, closed=closed)
return ShapeContour(List(n) { Segment2D(points[it], c1s[it], c2s[it], points[(it+1)%m]) }, closed=closed)
}
private fun thomas(a: Array<Double>, b: Array<Double>, c: Array<Double>, d: Array<Double>): Array<Double> {

View File

@@ -9,7 +9,7 @@ import kotlin.math.sign
import kotlin.math.sqrt
private fun Segment.splitOnExtrema(): List<Segment> {
private fun Segment2D.splitOnExtrema(): List<Segment2D> {
var extrema = extrema().toMutableList()
if (isStraight(0.05)) {
@@ -40,10 +40,10 @@ private fun Segment.splitOnExtrema(): List<Segment> {
}
}
private fun Segment.splitToSimple(step: Double): List<Segment> {
private fun Segment2D.splitToSimple(step: Double): List<Segment2D> {
var t1 = 0.0
var t2 = 0.0
val result = mutableListOf<Segment>()
val result = mutableListOf<Segment2D>()
while (t2 <= 1.0) {
t2 = t1 + step
while (t2 <= 1.0 + step) {
@@ -72,15 +72,15 @@ private fun Segment.splitToSimple(step: Double): List<Segment> {
}
fun Segment.reduced(stepSize: Double = 0.01): List<Segment> {
fun Segment2D.reduced(stepSize: Double = 0.01): List<Segment2D> {
val pass1 = splitOnExtrema()
//return pass1
return pass1.flatMap { it.splitToSimple(stepSize) }
}
fun Segment.scale(scale: Double, polarity: YPolarity) = scale(polarity) { scale }
fun Segment2D.scale(scale: Double, polarity: YPolarity) = scale(polarity) { scale }
fun Segment.scale(polarity: YPolarity, scale: (Double) -> Double): Segment {
fun Segment2D.scale(polarity: YPolarity, scale: (Double) -> Double): Segment2D {
if (control.size == 1) {
return cubic.scale(polarity, scale)
}
@@ -111,19 +111,19 @@ fun Segment.scale(polarity: YPolarity, scale: (Double) -> Double): Segment {
}
}
fun Segment.offset(
fun Segment2D.offset(
distance: Double,
stepSize: Double = 0.01,
yPolarity: YPolarity = YPolarity.CW_NEGATIVE_Y
): List<Segment> {
): List<Segment2D> {
return if (linear) {
val n = normal(0.0, yPolarity)
if (distance > 0.0) {
listOf(Segment(start + distance * n, end + distance * n))
listOf(Segment2D(start + distance * n, end + distance * n))
} else {
val d = direction()
val s = distance.coerceAtMost(length / 2.0)
val candidate = Segment(
val candidate = Segment2D(
start - s * d + distance * n,
end + s * d + distance * n
)
@@ -226,12 +226,12 @@ fun ShapeContour.offset(distance: Double, joinType: SegmentJoin = SegmentJoin.RO
var final = candidateContour.removeLoops()
if (postProc && !final.empty) {
val head = Segment(
val head = Segment2D(
segments[0].start + segments[0].normal(0.0)
.perpendicular(polarity) * 1000.0, segments[0].start
).offset(distance).firstOrNull()?.copy(end = final.segments[0].start)?.contour
val tail = Segment(
val tail = Segment2D(
segments.last().end,
segments.last().end - segments.last().normal(1.0)
.perpendicular(polarity) * 1000.0

View File

@@ -1,11 +1,10 @@
package org.openrndr.extra.shapes.operators
import org.openrndr.math.mod_
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour
fun ShapeContour.bulgeSegments(distortion: (index: Int, segment: Segment) -> Double): ShapeContour {
fun ShapeContour.bulgeSegments(distortion: (index: Int, segment: Segment2D) -> Double): ShapeContour {
val c = contour {
moveTo(position(0.0))
var index = 0

View File

@@ -7,15 +7,15 @@ import kotlin.math.abs
import kotlin.math.sign
import kotlin.math.sqrt
private fun Segment.linearSub(l0: Double, l1: Double): Segment {
private fun Segment2D.linearSub(l0: Double, l1: Double): Segment2D {
return sub(l0 / length, l1 / length)
}
private fun Segment.linearPosition(l: Double): Vector2 {
private fun Segment2D.linearPosition(l: Double): Vector2 {
return position((l / length).coerceIn(0.0, 1.0))
}
private fun pickLength(leftLength: Double, rightLength: Double, s0: Segment, s1: Segment): Double {
private fun pickLength(leftLength: Double, rightLength: Double, s0: Segment2D, s1: Segment2D): Double {
val p3 = s1.end
val p2 = s0.end
val p1 = s0.start
@@ -36,8 +36,8 @@ private fun pickLength(leftLength: Double, rightLength: Double, s0: Segment, s1:
* @param chamfer the chamfer function to apply
*/
fun ShapeContour.chamferCorners(
lengths: (index: Int, left: Segment, right: Segment) -> Double,
expands: (index: Int, left: Segment, right: Segment) -> Double = { _, _, _ -> 0.0 },
lengths: (index: Int, left: Segment2D, right: Segment2D) -> Double,
expands: (index: Int, left: Segment2D, right: Segment2D) -> Double = { _, _, _ -> 0.0 },
clip: Boolean = true,
angleThreshold: Double = 180.0,
chamfer: ContourBuilder.(p1: Vector2, p2: Vector2, p3: Vector2) -> Unit

View File

@@ -0,0 +1,10 @@
package org.openrndr.extra.shapes.rectify
import org.openrndr.shape.Path3D
/** create a rectified contour
* @param distanceTolerance distance tolerance to use, 0.5 is the default distance tolerance
* @param lengthScale used to compute the size of the LUT, default value is 1.0
**/
fun Path3D.rectified(distanceTolerance: Double = 0.5, lengthScale: Double = 1.0): RectifiedPath3D =
RectifiedPath3D(this, distanceTolerance, lengthScale)

View File

@@ -1,152 +1,53 @@
package org.openrndr.extra.shapes.rectify
import org.openrndr.extra.shapes.utilities.splitAt
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.clamp
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour
import kotlin.math.floor
/**
* RectifiedContour provides an approximately uniform parameterization for [ShapeContour]
*/
class RectifiedContour(val contour: ShapeContour, distanceTolerance: Double = 0.5, lengthScale: Double = 1.0) {
val points =
contour.equidistantPositionsWithT((contour.length * lengthScale).toInt().coerceAtLeast(2), distanceTolerance)
val intervals by lazy {
points.zipWithNext().map {
Pair(it.first.second, it.second.second)
}
}
private fun safe(t: Double): Double {
return if (contour.closed) {
t.mod(1.0)
} else {
t.clamp(0.0, 1.0)
}
}
/**
* computes a rectified t-value for [contour]
*/
fun rectify(t: Double): Double {
if (contour.empty) {
return 0.0
} else {
if (t <= 0.0) {
return 0.0
}
val fi = t * (points.size - 1.0)
val fr = fi.mod(1.0)
val i0 = fi.toInt()
val i1 = i0 + 1
return if (i0 >= points.size - 1) {
1.0
} else {
(points[i0].second * (1.0 - fr) + points[i1].second * fr)
}
}
}
fun inverseRectify(t: Double): Double {
if (contour.empty) {
return 0.0
} else {
if (t <= 0.0) {
return 0.0
} else if (t >= 1.0) {
return 1.0
} else {
val index = intervals.binarySearch {
if (t < it.first) {
1
} else if (t > it.second) {
-1
} else {
0
}
}
val t0 = t - intervals[index].first
val dt = intervals[index].second - intervals[index].first
val f = t0 / dt
val f0 = index.toDouble() / intervals.size
val f1 = (index + 1.0) / intervals.size
return f0 * (1.0 - f) + f1 * f
}
}
}
fun position(t: Double): Vector2 {
return if (contour.empty) {
Vector2.INFINITY
} else {
contour.position(rectify(safe(t)))
}
}
class RectifiedContour(contour: ShapeContour, distanceTolerance: Double = 0.5, lengthScale: Double = 1.0) :
RectifiedPath<Vector2>(contour, distanceTolerance, lengthScale) {
fun velocity(t: Double): Vector2 {
return if (contour.empty) {
return if (path.empty) {
Vector2.ZERO
} else {
val (segment, st) = contour.segment(rectify(safe(t)))
contour.segments[segment].direction(st)
val (segment, st) = path.segment(rectify(safe(t)))
path.segments[segment].direction(st)
}
}
fun normal(t: Double): Vector2 {
return if (contour.empty) {
return if (path.empty) {
Vector2.UNIT_Y
} else {
contour.normal(rectify(safe(t)))
(path as ShapeContour).normal(rectify(safe(t)))
}
}
fun pose(t: Double): Matrix44 {
return if (contour.empty) {
path as ShapeContour
return if (path.empty) {
Matrix44.IDENTITY
} else {
contour.pose(rectify(safe(t)))
path.pose(rectify(safe(t)))
}
}
fun sub(t0: Double, t1: Double): ShapeContour {
if (contour.empty) {
override fun sub(t0: Double, t1: Double): ShapeContour {
path as ShapeContour
if (path.empty) {
return ShapeContour.EMPTY
}
return if (contour.closed) {
contour.sub(rectify(t0.mod(1.0)) + floor(t0), rectify(t1.mod(1.0)) + floor(t1))
return if (path.closed) {
path.sub(rectify(t0.mod(1.0)) + floor(t0), rectify(t1.mod(1.0)) + floor(t1))
} else {
contour.sub(rectify(t0), rectify(t1))
path.sub(rectify(t0), rectify(t1))
}
}
/**
* Split contour at [ascendingTs]
* @since orx 0.4.4
*/
fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<ShapeContour> {
return contour.splitAt(ascendingTs.map { rectify(it) }, weldEpsilon)
override fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double): List<ShapeContour> {
@Suppress("UNCHECKED_CAST")
return super.splitAt(ascendingTs, weldEpsilon) as List<ShapeContour>
}
}
/** create a rectified contour
* @param distanceTolerance distance tolerance to use, 0.5 is the default distance tolerance
* @param lengthScale used to compute the size of the LUT, default value is 1.0
**/
fun ShapeContour.rectified(distanceTolerance: Double = 0.5, lengthScale: Double = 1.0): RectifiedContour {
return RectifiedContour(this, distanceTolerance, lengthScale)
}
/** create a rectified contour
* @param distanceTolerance distance tolerance to use, 0.5 is the default distance tolerance
* @param lengthScale used to compute the size of the LUT, default value is 1.0
*
* */
fun Segment.rectified(distanceTolerance: Double = 0.5, lengthScale: Double = 1.0): RectifiedContour {
return RectifiedContour(this.contour, distanceTolerance, lengthScale)
}

View File

@@ -0,0 +1,104 @@
package org.openrndr.extra.shapes.rectify
import org.openrndr.extra.shapes.utilities.splitAt
import org.openrndr.extra.shapes.utilities.splitAtBase
import org.openrndr.math.EuclideanVector
import org.openrndr.math.clamp
import org.openrndr.shape.Path
import org.openrndr.shape.ShapeContour
/**
* RectifiedContour provides an approximately uniform parameterization for [ShapeContour]
*/
abstract class RectifiedPath<T : EuclideanVector<T>>(
val path: Path<T>,
distanceTolerance: Double = 0.5,
lengthScale: Double = 1.0
) {
val points =
path.equidistantPositionsWithT((path.length * lengthScale).toInt().coerceAtLeast(2), distanceTolerance)
val intervals by lazy {
points.zipWithNext().map {
Pair(it.first.second, it.second.second)
}
}
internal fun safe(t: Double): Double {
return if (path.closed) {
t.mod(1.0)
} else {
t.clamp(0.0, 1.0)
}
}
/**
* computes a rectified t-value for [path]
*/
fun rectify(t: Double): Double {
if (path.empty) {
return 0.0
} else {
if (t <= 0.0) {
return 0.0
}
val fi = t * (points.size - 1.0)
val fr = fi.mod(1.0)
val i0 = fi.toInt()
val i1 = i0 + 1
return if (i0 >= points.size - 1) {
1.0
} else {
(points[i0].second * (1.0 - fr) + points[i1].second * fr)
}
}
}
fun inverseRectify(t: Double): Double {
if (path.empty) {
return 0.0
} else {
if (t <= 0.0) {
return 0.0
} else if (t >= 1.0) {
return 1.0
} else {
val index = intervals.binarySearch {
if (t < it.first) {
1
} else if (t > it.second) {
-1
} else {
0
}
}
val t0 = t - intervals[index].first
val dt = intervals[index].second - intervals[index].first
val f = t0 / dt
val f0 = index.toDouble() / intervals.size
val f1 = (index + 1.0) / intervals.size
return f0 * (1.0 - f) + f1 * f
}
}
}
fun position(t: Double): T {
return if (path.empty) {
path.infinity
} else {
path.position(rectify(safe(t)))
}
}
abstract fun sub(t0: Double, t1: Double): Path<T>
/**
* Split contour at [ascendingTs]
* @since orx 0.4.4
*/
open fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<Path<T>> {
return path.splitAtBase(ascendingTs.map { rectify(it) }, weldEpsilon)
}
}

View File

@@ -0,0 +1,27 @@
package org.openrndr.extra.shapes.rectify
import org.openrndr.math.Vector3
import org.openrndr.shape.Path3D
import kotlin.math.floor
class RectifiedPath3D(contour: Path3D, distanceTolerance: Double = 0.5, lengthScale: Double = 1.0) :
RectifiedPath<Vector3>(contour, distanceTolerance, lengthScale) {
override fun sub(t0: Double, t1: Double): Path3D {
path as Path3D
if (path.empty) {
return Path3D(emptyList(), false)
}
return if (path.closed) {
path.sub(rectify(t0.mod(1.0)) + floor(t0), rectify(t1.mod(1.0)) + floor(t1))
} else {
path.sub(rectify(t0), rectify(t1))
}
}
override fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double): List<Path3D> {
@Suppress("UNCHECKED_CAST")
return super.splitAt(ascendingTs, weldEpsilon) as List<Path3D>
}
}

View File

@@ -0,0 +1,21 @@
package org.openrndr.extra.shapes.rectify
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
/** create a rectified contour
* @param distanceTolerance distance tolerance to use, 0.5 is the default distance tolerance
* @param lengthScale used to compute the size of the LUT, default value is 1.0
**/
fun ShapeContour.rectified(distanceTolerance: Double = 0.5, lengthScale: Double = 1.0): RectifiedContour {
return RectifiedContour(this, distanceTolerance, lengthScale)
}
/** create a rectified contour
* @param distanceTolerance distance tolerance to use, 0.5 is the default distance tolerance
* @param lengthScale used to compute the size of the LUT, default value is 1.0
*
* */
fun Segment2D.rectified(distanceTolerance: Double = 0.5, lengthScale: Double = 1.0): RectifiedContour {
return RectifiedContour(this.contour, distanceTolerance, lengthScale)
}

View File

@@ -287,7 +287,7 @@ fun List<Vector3>.catmullRom(alpha: Double = 0.5, closed: Boolean) = CatmullRomC
/** Converts spline to a [Segment]. */
fun CatmullRom2.toSegment(): Segment {
fun CatmullRom2.toSegment(): Segment2D {
val d1a2 = (p1 - p0).length.pow(2 * alpha)
val d2a2 = (p2 - p1).length.pow(2 * alpha)
val d3a2 = (p3 - p2).length.pow(2 * alpha)
@@ -300,7 +300,7 @@ fun CatmullRom2.toSegment(): Segment {
val b2 = (p1 * d3a2 - p3 * d2a2 + p2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
val b3 = p2
return Segment(b0, b1, b2, b3)
return Segment2D(b0, b1, b2, b3)
}

View File

@@ -2,14 +2,14 @@ package org.openrndr.extra.shapes.tunni
import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.intersection
/**
* Find the Tunni point for the [Segment]
* @since orx 0.4.5
*/
val Segment.tunniPoint: Vector2
val Segment2D.tunniPoint: Vector2
get() {
val c = this.cubic
val ac = LineSegment(c.start, c.control[0])
@@ -23,7 +23,7 @@ val Segment.tunniPoint: Vector2
* Find the Tunni line for the [Segment]
* @since orx 0.4.5
*/
val Segment.tunniLine: LineSegment
val Segment2D.tunniLine: LineSegment
get() {
val c = this.cubic
return LineSegment(c.control[0], c.control[1])
@@ -33,7 +33,7 @@ val Segment.tunniLine: LineSegment
* Find a new segment that has [tunniPoint] as its Tunni-point
* @since orx 0.4.5
*/
fun Segment.withTunniPoint(tunniPoint: Vector2): Segment {
fun Segment2D.withTunniPoint(tunniPoint: Vector2): Segment2D {
val ha = (start + tunniPoint) / 2.0
val hb = (end + tunniPoint) / 2.0
val hpa = ha + this.cubic.control[1] - end
@@ -57,7 +57,7 @@ fun Segment.withTunniPoint(tunniPoint: Vector2): Segment {
* Find a segment for which [pointOnLine] lies on its Tunni-line
* @since orx 0.4.5
*/
fun Segment.withTunniLine(pointOnLine: Vector2): Segment {
fun Segment2D.withTunniLine(pointOnLine: Vector2): Segment2D {
val ls = LineSegment(pointOnLine, pointOnLine + this.cubic.control[0] - this.cubic.control[1])
val ac0 = LineSegment(start, this.cubic.control[0])
val bc1 = LineSegment(end, this.cubic.control[1])

View File

@@ -4,7 +4,6 @@ import org.openrndr.extra.shapes.adjust.ContourAdjusterEdge
import org.openrndr.extra.shapes.adjust.ContourEdge
import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour

View File

@@ -0,0 +1,31 @@
package org.openrndr.extra.shapes.utilities
import org.openrndr.shape.Path3D
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour
import org.openrndr.shape.path3D
/**
* Create a [Path3D] from a list of paths
*/
fun Path3D.Companion.fromPaths(contours: List<Path3D>, closed: Boolean, connectEpsilon:Double=1E-6) : Path3D {
@Suppress("NAME_SHADOWING") val contours = contours.filter { !it.empty }
if (contours.isEmpty()) {
return EMPTY
}
return path3D {
moveTo(contours.first().position(0.0))
for (c in contours.windowed(2,1,true)) {
copy(c[0])
if (c.size == 2) {
val d = c[0].position(1.0).distanceTo(c[1].position(0.0))
if (d > connectEpsilon ) {
lineTo(c[1].position(0.0))
}
}
}
if (closed) {
close()
}
}
}

View File

@@ -1,14 +1,20 @@
package org.openrndr.extra.shapes.utilities
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour
import org.openrndr.math.EuclideanVector
import org.openrndr.shape.*
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> {
fun Path3D.splitAt(segmentIndex: Double, segmentT: Double): List<Path3D> {
val t = (1.0 / segments.size) * (segmentIndex + segmentT)
return splitAt(listOf(t))
}
fun <T : EuclideanVector<T>> Path<T>.splitAtBase(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<Path<T>> {
if (empty || ascendingTs.isEmpty()) {
return listOf(this)
}
@@ -18,7 +24,20 @@ fun ShapeContour.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6):
}
}
fun Segment.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<Segment> {
fun ShapeContour.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<ShapeContour> {
@Suppress("UNCHECKED_CAST")
return splitAtBase(ascendingTs, weldEpsilon) as List<ShapeContour>
}
fun Path3D.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<Path3D> {
@Suppress("UNCHECKED_CAST")
return splitAtBase(ascendingTs, weldEpsilon) as List<Path3D>
}
fun <T : EuclideanVector<T>> BezierSegment<T>.splitAtBase(
ascendingTs: List<Double>,
weldEpsilon: Double = 1E-6
): List<BezierSegment<T>> {
if (ascendingTs.isEmpty()) {
return listOf(this)
}
@@ -28,3 +47,15 @@ fun Segment.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List
sub(it[0], it[1])
}
}
fun Segment2D.splitAt(ascendingTs: List<Double>,
weldEpsilon: Double = 1E-6) : List<Segment2D> {
@Suppress("UNCHECKED_CAST")
return splitAtBase(ascendingTs, weldEpsilon) as List<Segment2D>
}
fun Segment3D.splitAt(ascendingTs: List<Double>,
weldEpsilon: Double = 1E-6) : List<Segment3D> {
@Suppress("UNCHECKED_CAST")
return splitAtBase(ascendingTs, weldEpsilon) as List<Segment3D>
}

View File

@@ -0,0 +1,34 @@
package rectify
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.noise.scatter
import org.openrndr.extra.shapes.hobbycurve.hobbyCurve
import org.openrndr.extra.shapes.rectify.rectified
import kotlin.random.Random
fun main() {
application {
configure {
width = 720
height = 720
}
program {
val points = drawer.bounds.scatter(80.0, distanceToEdge = 100.0, random = Random(0))
val curve = hobbyCurve(points, closed = true)
val rectified = curve.rectified()
extend {
drawer.clear(ColorRGBa.BLACK)
drawer.fill = null
drawer.stroke = ColorRGBa.GRAY
drawer.contour(curve)
val points = (0 until 100).map {
rectified.position(it/100.0)
}
drawer.circles(points, 5.0)
}
}
}
}

View File

@@ -0,0 +1,49 @@
package rectify
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.isolated
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.sphereMesh
import org.openrndr.extra.noise.uniformRing
import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.math.Vector3
import org.openrndr.shape.path3D
fun main() {
application {
configure {
width = 720
height = 720
multisample = WindowMultisample.SampleCount(4)
}
program {
val p = path3D {
moveTo(0.0, 0.0, 0.0)
for (i in 0 until 10) {
curveTo(
Vector3.uniformRing(0.1, 1.0)*10.0,
Vector3.uniformRing(0.1, 1.0)*10.0,
Vector3.uniformRing(0.1, 1.0)*10.0
)
}
}
val pr = p.rectified(0.01, 100.0)
val sphere = sphereMesh(radius = 0.1)
extend(Orbital())
extend {
drawer.stroke = ColorRGBa.PINK
for (i in 0 until 500) {
drawer.isolated {
drawer.translate(pr.position(i/499.0))
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
}
}
drawer.path(p)
}
}
}
}

View File

@@ -141,7 +141,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
var prevQuadCtrlPoint: Vector2? = null
val contours = compounds().map { compound ->
val segments = mutableListOf<Segment>()
val segments = mutableListOf<Segment2D>()
var closed = false
// If an argument is invalid, an error is logged,
// further interpreting is stopped and compound is returned as-is.
@@ -207,7 +207,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
// Following points are implicit lineto arguments
segments += points.drop(1).map {
Segment(cursor, it).apply {
Segment2D(cursor, it).apply {
cursor = it
}
}
@@ -219,21 +219,21 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
// Following points are implicit lineto arguments
segments += points.drop(1).map {
Segment(cursor, cursor + it).apply {
Segment2D(cursor, cursor + it).apply {
cursor += it
}
}
}
"L" -> {
segments += points!!.map {
Segment(cursor, it).apply {
Segment2D(cursor, it).apply {
cursor = it
}
}
}
"l" -> {
segments += points!!.map {
Segment(cursor, cursor + it).apply {
Segment2D(cursor, cursor + it).apply {
cursor += it
}
}
@@ -241,7 +241,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"H" -> {
segments += command.operands.map {
val target = Vector2(it, cursor.y)
Segment(cursor, target).apply {
Segment2D(cursor, target).apply {
cursor = target
}
}
@@ -249,7 +249,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"h" -> {
segments += command.operands.map {
val target = cursor + Vector2(it, 0.0)
Segment(cursor, target).apply {
Segment2D(cursor, target).apply {
cursor = target
}
}
@@ -257,7 +257,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"V" -> {
segments += command.operands.map {
val target = Vector2(cursor.x, it)
Segment(cursor, target).apply {
Segment2D(cursor, target).apply {
cursor = target
}
}
@@ -265,7 +265,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"v" -> {
segments += command.operands.map {
val target = cursor + Vector2(0.0, it)
Segment(cursor, target).apply {
Segment2D(cursor, target).apply {
cursor = target
}
}
@@ -277,7 +277,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach
} else {
val (cp1, cp2, target) = it
Segment(cursor, cp1, cp2, target).also {
Segment2D(cursor, cp1, cp2, target).also {
cursor = target
prevCubicCtrlPoint = cp2
}
@@ -291,7 +291,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach
} else {
val (cp1, cp2, target) = it.map { v -> cursor + v }
Segment(cursor, cp1, cp2, target).apply {
Segment2D(cursor, cp1, cp2, target).apply {
cursor = target
prevCubicCtrlPoint = cp2
}
@@ -306,7 +306,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
} else {
val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
val (cp2, target) = it
Segment(cursor, cp1, cp2, target).also {
Segment2D(cursor, cp1, cp2, target).also {
cursor = target
prevCubicCtrlPoint = cp2
}
@@ -321,7 +321,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
} else {
val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
val (cp2, target) = it.map { v -> cursor + v }
Segment(cursor, cp1, cp2, target).also {
Segment2D(cursor, cp1, cp2, target).also {
cursor = target
prevCubicCtrlPoint = cp2
}
@@ -335,7 +335,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach
} else {
val (cp, target) = it
Segment(cursor, cp, target).also {
Segment2D(cursor, cp, target).also {
cursor = target
prevQuadCtrlPoint = cp
}
@@ -349,7 +349,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach
} else {
val (cp, target) = it.map { v -> cursor + v }
Segment(cursor, cp, target).also {
Segment2D(cursor, cp, target).also {
cursor = target
prevQuadCtrlPoint = cp
}
@@ -359,7 +359,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"T" -> {
points!!.forEach {
val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
Segment(cursor, cp, it).also { _ ->
Segment2D(cursor, cp, it).also { _ ->
cursor = it
prevQuadCtrlPoint = cp
}
@@ -368,7 +368,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"t" -> {
points!!.forEach {
val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
Segment(cursor, cp, cursor + it).also { _ ->
Segment2D(cursor, cp, cursor + it).also { _ ->
cursor = it
prevQuadCtrlPoint = cp
}
@@ -376,7 +376,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
}
"Z", "z" -> {
if ((cursor - anchor).length >= 0.001) {
segments += Segment(cursor, anchor)
segments += Segment2D(cursor, anchor)
}
cursor = anchor
closed = true

View File

@@ -4,7 +4,6 @@ plugins {
kotlin {
sourceSets {
@Suppress("UNUSED_VARIABLE")
val commonMain by getting {
dependencies {
api(libs.openrndr.math)
@@ -12,12 +11,18 @@ kotlin {
implementation(project(":orx-noise"))
}
}
val commonTest by getting {
dependencies {
implementation(project(":orx-shapes"))
implementation(libs.openrndr.shape)
}
}
@Suppress("UNUSED_VARIABLE")
val jvmDemo by getting {
dependencies {
implementation(project(":orx-shapes"))
implementation(project(":orx-noise"))
implementation(libs.openrndr.shape)
}
}
}

View File

@@ -1,6 +1,7 @@
import org.openrndr.extra.triangulation.Delaunay
import org.openrndr.shape.Circle
import org.openrndr.shape.Rectangle
import kotlin.test.Test
import kotlin.test.assertTrue

View File

@@ -3,7 +3,7 @@ package org.openrndr.extra.turtle
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.buildTransform
import org.openrndr.shape.Segment
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour
fun Turtle.contour(contour: ShapeContour, alignTangent: Boolean = true) {
@@ -16,7 +16,7 @@ fun Turtle.contour(contour: ShapeContour, alignTangent: Boolean = true) {
}
fun Turtle.segment(
segment: Segment,
segment: Segment2D,
alignTangent: Boolean = true,
externalAlignTransform: Matrix44 = Matrix44.IDENTITY
): Matrix44 {