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, c1: Vector2,
end: Vector2, end: Vector2,
insert: Boolean = true insert: Boolean = true
) = segment(Segment(start, c0, c1, end), insert) ) = segment(Segment2D(start, c0, c1, end), insert)
fun segment( fun segment(
start: Vector2, start: Vector2,
c0: Vector2, c0: Vector2,
end: Vector2, end: Vector2,
insert: Boolean = true insert: Boolean = true
) = segment(Segment(start, c0, end), insert) ) = segment(Segment2D(start, c0, end), insert)
fun segment( fun segment(
start: Vector2, start: Vector2,
end: Vector2, end: Vector2,
insert: Boolean = true insert: Boolean = true
) = segment(Segment(start, end), insert) ) = segment(Segment2D(start, end), insert)
fun segment( fun segment(
segment: Segment, segment: Segment2D,
insert: Boolean = true insert: Boolean = true
) = contour(segment.contour, insert) ) = contour(segment.contour, insert)
fun segments( fun segments(
segments: List<Segment>, segments: List<Segment2D>,
insert: Boolean = true insert: Boolean = true
) = segments.map { ) = segments.map {
segment(it, insert) segment(it, insert)

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,7 @@ kotlin {
implementation(project(":orx-triangulation")) implementation(project(":orx-triangulation"))
implementation(project(":orx-shapes")) implementation(project(":orx-shapes"))
implementation(project(":orx-noise")) 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.collections.pop
import org.openrndr.extra.shapes.vertex.ContourVertex import org.openrndr.extra.shapes.vertex.ContourVertex
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.Segment import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
class ContourAdjusterStatus( class ContourAdjusterStatus(
val contour: ShapeContour, val contour: ShapeContour,
val selectedSegments: List<Segment>, val selectedSegments: List<Segment2D>,
val selectedPoints: List<Vector2> 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.Matrix44
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.math.transforms.buildTransform 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.SegmentType
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import kotlin.math.abs import kotlin.math.abs
@@ -151,7 +151,7 @@ data class ContourEdge(
removeAt(segmentIndex) removeAt(segmentIndex)
if (segment.start.distanceTo(openContour.position(0.0)) > 1E-3) { 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++ insertIndex++
} }
for (s in openContour.segments) { for (s in openContour.segments) {
@@ -159,7 +159,7 @@ data class ContourEdge(
insertIndex++ insertIndex++
} }
if (segment.end.distanceTo(openContour.position(1.0)) > 1E-3) { 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) return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.Matrix44 import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle import org.openrndr.shape.Rectangle
import org.openrndr.shape.Segment import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import kotlin.random.Random import kotlin.random.Random
@@ -123,7 +123,7 @@ open class BezierPatchBase<C>(
cps[j] += points[i][j] * cs[i] 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 { fun vertical(u: Double): ShapeContour {
@@ -134,33 +134,33 @@ open class BezierPatchBase<C>(
cps[j] += points[j][i] * cs[i] 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 * Extract a sub-patch based on uv parameterization
*/ */
fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatchBase<C> { 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 c0 = Segment2D(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 c1 = Segment2D(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 c2 = Segment2D(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 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 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 d0 = Segment2D(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 d1 = Segment2D(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 d2 = Segment2D(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 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 return fromSegments<C>(d0, d1, d2, d3).transposed
} }
val contour: ShapeContour = ShapeContour( val contour: ShapeContour = ShapeContour(
listOf( listOf(
Segment(points[0][0], points[0][1], points[0][2], points[0][3]), Segment2D(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]), Segment2D(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]), Segment2D(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[3][0], points[2][0], points[1][0], points[0][0]),
), true ), true
) )
@@ -196,7 +196,7 @@ open class BezierPatchBase<C>(
} }
companion object { 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 { where C : AlgebraicColor<C>, C : ConvertibleToColorRGBa {
val c0c = c0.cubic val c0c = c0.cubic
val c1c = c1.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 * 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 c0c = c0.cubic
val c1c = c1.cubic val c1c = c1.cubic
val c2c = c2.cubic val c2c = c2.cubic
@@ -289,7 +289,7 @@ fun BezierPatch.distort(shapeContour: ShapeContour, referenceRectangle: Rectangl
val ns = position(s.x, s.y) val ns = position(s.x, s.y)
val nc0 = position(c0.x, c0.y) val nc0 = position(c0.x, c0.y)
val nc1 = position(c1.x, c1.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) 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 * Extract a sub-patch based on uv parameterization
*/ */
fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatch3DBase<C> { 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 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) 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) 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) 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 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 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) 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) 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) 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 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 rb = b.rectified()
val sa = ra.splitForBlend(rb) val sa = ra.splitForBlend(rb)
val sb = rb.splitForBlend(ra) val sb = rb.splitForBlend(ra)
require(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.contour.segments.size}, ${sb.contour.segments.size}" "preprocessing for contours failed to produce equal number of segments. ${sa.path.segments.size}, ${sb.path.segments.size}"
} }
return ContourBlend(sa, sb) 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.RectifiedContour
import org.openrndr.extra.shapes.rectify.rectified import org.openrndr.extra.shapes.rectify.rectified
import org.openrndr.extra.shapes.utilities.fromContours import org.openrndr.extra.shapes.utilities.fromContours
import org.openrndr.shape.Segment2D
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
/** /**
* Split for blending with [other] * Split for blending with [other]
*/ */
fun RectifiedContour.splitForBlend(other: RectifiedContour): RectifiedContour { 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) } 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 { fun RectifiedContour.mix(other: RectifiedContour, blendFunction: (Double) -> Double): ShapeContour {
val n = this.contour.segments.size.toDouble() val n = this.path.segments.size.toDouble()
val segs = (this.contour.segments zip other.contour.segments).mapIndexed { index, it -> val segs = (this.path.segments zip other.path.segments).mapIndexed { index, it ->
val t0 = inverseRectify(index / n) val t0 = inverseRectify(index / n)
val t1 = inverseRectify((index + 1 / 3.0) / n) val t1 = inverseRectify((index + 1 / 3.0) / n)
val t2 = inverseRectify((index + 2 / 3.0) / n) val t2 = inverseRectify((index + 2 / 3.0) / n)
val t3 = inverseRectify((index + 1) / n) val t3 = inverseRectify((index + 1) / n)
(it.first as Segment2D).mix(it.second as Segment2D, blendFunction(t0), blendFunction(t1), blendFunction(t2), blendFunction(t3))
it.first.mix(it.second, 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 package org.openrndr.extra.shapes.blend
import org.openrndr.math.mix import org.openrndr.math.mix
import org.openrndr.shape.Segment import org.openrndr.shape.Segment2D
import org.openrndr.shape.Segment3D
/** /**
* Cubic segment mix * Cubic segment mix
@@ -11,14 +13,14 @@ import org.openrndr.shape.Segment
* @param f2 the mix factor for the second control point * @param f2 the mix factor for the second control point
* @param f3 the mix factor for the end 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 ac = this.cubic
val bc = other.cubic val bc = other.cubic
val acc = if (ac.corner) 1.0 else 0.0 val acc = if (ac.corner) 1.0 else 0.0
val bcc = if (bc.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.start.mix(bc.start, f0),
ac.control[0].mix(bc.control[0], f1), ac.control[0].mix(bc.control[0], f1),
ac.control[1].mix(bc.control[1], f2), 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 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] * Evaluates the bounds around all [Segment] instances in the [Iterable]
*/ */
val Iterable<Segment>.bounds : Rectangle val Iterable<Segment2D>.bounds : Rectangle
@JvmName("segmentBounds") @JvmName("segmentBounds")
get() = map { get() = map {
it.bounds it.bounds

View File

@@ -2,7 +2,7 @@ package org.openrndr.extra.shapes.hobbycurve
// Code adapted from http://weitz.de/hobby/ // Code adapted from http://weitz.de/hobby/
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.Segment import org.openrndr.shape.Segment2D
import org.openrndr.shape.Shape import org.openrndr.shape.Shape
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import kotlin.math.atan2 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) 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> { 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 import kotlin.math.sqrt
private fun Segment.splitOnExtrema(): List<Segment> { private fun Segment2D.splitOnExtrema(): List<Segment2D> {
var extrema = extrema().toMutableList() var extrema = extrema().toMutableList()
if (isStraight(0.05)) { 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 t1 = 0.0
var t2 = 0.0 var t2 = 0.0
val result = mutableListOf<Segment>() val result = mutableListOf<Segment2D>()
while (t2 <= 1.0) { while (t2 <= 1.0) {
t2 = t1 + step t2 = t1 + step
while (t2 <= 1.0 + 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() val pass1 = splitOnExtrema()
//return pass1 //return pass1
return pass1.flatMap { it.splitToSimple(stepSize) } 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) { if (control.size == 1) {
return cubic.scale(polarity, scale) 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, distance: Double,
stepSize: Double = 0.01, stepSize: Double = 0.01,
yPolarity: YPolarity = YPolarity.CW_NEGATIVE_Y yPolarity: YPolarity = YPolarity.CW_NEGATIVE_Y
): List<Segment> { ): List<Segment2D> {
return if (linear) { return if (linear) {
val n = normal(0.0, yPolarity) val n = normal(0.0, yPolarity)
if (distance > 0.0) { if (distance > 0.0) {
listOf(Segment(start + distance * n, end + distance * n)) listOf(Segment2D(start + distance * n, end + distance * n))
} else { } else {
val d = direction() val d = direction()
val s = distance.coerceAtMost(length / 2.0) val s = distance.coerceAtMost(length / 2.0)
val candidate = Segment( val candidate = Segment2D(
start - s * d + distance * n, start - s * d + distance * n,
end + 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() var final = candidateContour.removeLoops()
if (postProc && !final.empty) { if (postProc && !final.empty) {
val head = Segment( val head = Segment2D(
segments[0].start + segments[0].normal(0.0) segments[0].start + segments[0].normal(0.0)
.perpendicular(polarity) * 1000.0, segments[0].start .perpendicular(polarity) * 1000.0, segments[0].start
).offset(distance).firstOrNull()?.copy(end = final.segments[0].start)?.contour ).offset(distance).firstOrNull()?.copy(end = final.segments[0].start)?.contour
val tail = Segment( val tail = Segment2D(
segments.last().end, segments.last().end,
segments.last().end - segments.last().normal(1.0) segments.last().end - segments.last().normal(1.0)
.perpendicular(polarity) * 1000.0 .perpendicular(polarity) * 1000.0

View File

@@ -1,11 +1,10 @@
package org.openrndr.extra.shapes.operators package org.openrndr.extra.shapes.operators
import org.openrndr.math.mod_ import org.openrndr.shape.Segment2D
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import org.openrndr.shape.contour 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 { val c = contour {
moveTo(position(0.0)) moveTo(position(0.0))
var index = 0 var index = 0

View File

@@ -7,15 +7,15 @@ import kotlin.math.abs
import kotlin.math.sign import kotlin.math.sign
import kotlin.math.sqrt 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) 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)) 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 p3 = s1.end
val p2 = s0.end val p2 = s0.end
val p1 = s0.start 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 * @param chamfer the chamfer function to apply
*/ */
fun ShapeContour.chamferCorners( fun ShapeContour.chamferCorners(
lengths: (index: Int, left: Segment, right: Segment) -> Double, lengths: (index: Int, left: Segment2D, right: Segment2D) -> Double,
expands: (index: Int, left: Segment, right: Segment) -> Double = { _, _, _ -> 0.0 }, expands: (index: Int, left: Segment2D, right: Segment2D) -> Double = { _, _, _ -> 0.0 },
clip: Boolean = true, clip: Boolean = true,
angleThreshold: Double = 180.0, angleThreshold: Double = 180.0,
chamfer: ContourBuilder.(p1: Vector2, p2: Vector2, p3: Vector2) -> Unit 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 package org.openrndr.extra.shapes.rectify
import org.openrndr.extra.shapes.utilities.splitAt
import org.openrndr.math.Matrix44 import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.math.clamp
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour import org.openrndr.shape.ShapeContour
import kotlin.math.floor import kotlin.math.floor
/** class RectifiedContour(contour: ShapeContour, distanceTolerance: Double = 0.5, lengthScale: Double = 1.0) :
* RectifiedContour provides an approximately uniform parameterization for [ShapeContour] RectifiedPath<Vector2>(contour, distanceTolerance, lengthScale) {
*/
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)))
}
}
fun velocity(t: Double): Vector2 { fun velocity(t: Double): Vector2 {
return if (contour.empty) { return if (path.empty) {
Vector2.ZERO Vector2.ZERO
} else { } else {
val (segment, st) = contour.segment(rectify(safe(t))) val (segment, st) = path.segment(rectify(safe(t)))
contour.segments[segment].direction(st) path.segments[segment].direction(st)
} }
} }
fun normal(t: Double): Vector2 { fun normal(t: Double): Vector2 {
return if (contour.empty) { return if (path.empty) {
Vector2.UNIT_Y Vector2.UNIT_Y
} else { } else {
contour.normal(rectify(safe(t))) (path as ShapeContour).normal(rectify(safe(t)))
} }
} }
fun pose(t: Double): Matrix44 { fun pose(t: Double): Matrix44 {
return if (contour.empty) { path as ShapeContour
return if (path.empty) {
Matrix44.IDENTITY Matrix44.IDENTITY
} else { } else {
contour.pose(rectify(safe(t))) path.pose(rectify(safe(t)))
} }
} }
fun sub(t0: Double, t1: Double): ShapeContour { override fun sub(t0: Double, t1: Double): ShapeContour {
if (contour.empty) { path as ShapeContour
if (path.empty) {
return ShapeContour.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 { } else {
contour.sub(rectify(t0), rectify(t1)) path.sub(rectify(t0), rectify(t1))
} }
} }
/** override fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double): List<ShapeContour> {
* Split contour at [ascendingTs] @Suppress("UNCHECKED_CAST")
* @since orx 0.4.4 return super.splitAt(ascendingTs, weldEpsilon) as List<ShapeContour>
*/
fun splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List<ShapeContour> {
return contour.splitAt(ascendingTs.map { rectify(it) }, weldEpsilon)
} }
} }
/** 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]. */ /** Converts spline to a [Segment]. */
fun CatmullRom2.toSegment(): Segment { fun CatmullRom2.toSegment(): Segment2D {
val d1a2 = (p1 - p0).length.pow(2 * alpha) val d1a2 = (p1 - p0).length.pow(2 * alpha)
val d2a2 = (p2 - p1).length.pow(2 * alpha) val d2a2 = (p2 - p1).length.pow(2 * alpha)
val d3a2 = (p3 - p2).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 b2 = (p1 * d3a2 - p3 * d2a2 + p2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
val b3 = p2 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.math.Vector2
import org.openrndr.shape.LineSegment import org.openrndr.shape.LineSegment
import org.openrndr.shape.Segment import org.openrndr.shape.Segment2D
import org.openrndr.shape.intersection import org.openrndr.shape.intersection
/** /**
* Find the Tunni point for the [Segment] * Find the Tunni point for the [Segment]
* @since orx 0.4.5 * @since orx 0.4.5
*/ */
val Segment.tunniPoint: Vector2 val Segment2D.tunniPoint: Vector2
get() { get() {
val c = this.cubic val c = this.cubic
val ac = LineSegment(c.start, c.control[0]) val ac = LineSegment(c.start, c.control[0])
@@ -23,7 +23,7 @@ val Segment.tunniPoint: Vector2
* Find the Tunni line for the [Segment] * Find the Tunni line for the [Segment]
* @since orx 0.4.5 * @since orx 0.4.5
*/ */
val Segment.tunniLine: LineSegment val Segment2D.tunniLine: LineSegment
get() { get() {
val c = this.cubic val c = this.cubic
return LineSegment(c.control[0], c.control[1]) 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 * Find a new segment that has [tunniPoint] as its Tunni-point
* @since orx 0.4.5 * @since orx 0.4.5
*/ */
fun Segment.withTunniPoint(tunniPoint: Vector2): Segment { fun Segment2D.withTunniPoint(tunniPoint: Vector2): Segment2D {
val ha = (start + tunniPoint) / 2.0 val ha = (start + tunniPoint) / 2.0
val hb = (end + tunniPoint) / 2.0 val hb = (end + tunniPoint) / 2.0
val hpa = ha + this.cubic.control[1] - end 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 * Find a segment for which [pointOnLine] lies on its Tunni-line
* @since orx 0.4.5 * @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 ls = LineSegment(pointOnLine, pointOnLine + this.cubic.control[0] - this.cubic.control[1])
val ac0 = LineSegment(start, this.cubic.control[0]) val ac0 = LineSegment(start, this.cubic.control[0])
val bc1 = LineSegment(end, this.cubic.control[1]) 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.extra.shapes.adjust.ContourEdge
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
import org.openrndr.shape.LineSegment import org.openrndr.shape.LineSegment
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour 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 package org.openrndr.extra.shapes.utilities
import org.openrndr.shape.Segment import org.openrndr.math.EuclideanVector
import org.openrndr.shape.ShapeContour import org.openrndr.shape.*
fun ShapeContour.splitAt(segmentIndex: Double, segmentT: Double): List<ShapeContour> { fun ShapeContour.splitAt(segmentIndex: Double, segmentT: Double): List<ShapeContour> {
val t = (1.0 / segments.size) * (segmentIndex + segmentT) val t = (1.0 / segments.size) * (segmentIndex + segmentT)
return splitAt(listOf(t)) 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()) { if (empty || ascendingTs.isEmpty()) {
return listOf(this) 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()) { if (ascendingTs.isEmpty()) {
return listOf(this) return listOf(this)
} }
@@ -27,4 +46,16 @@ fun Segment.splitAt(ascendingTs: List<Double>, weldEpsilon: Double = 1E-6): List
return ascendingTs.windowed(2, 1).map { return ascendingTs.windowed(2, 1).map {
sub(it[0], it[1]) 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 var prevQuadCtrlPoint: Vector2? = null
val contours = compounds().map { compound -> val contours = compounds().map { compound ->
val segments = mutableListOf<Segment>() val segments = mutableListOf<Segment2D>()
var closed = false var closed = false
// If an argument is invalid, an error is logged, // If an argument is invalid, an error is logged,
// further interpreting is stopped and compound is returned as-is. // 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 // Following points are implicit lineto arguments
segments += points.drop(1).map { segments += points.drop(1).map {
Segment(cursor, it).apply { Segment2D(cursor, it).apply {
cursor = it cursor = it
} }
} }
@@ -219,21 +219,21 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
// Following points are implicit lineto arguments // Following points are implicit lineto arguments
segments += points.drop(1).map { segments += points.drop(1).map {
Segment(cursor, cursor + it).apply { Segment2D(cursor, cursor + it).apply {
cursor += it cursor += it
} }
} }
} }
"L" -> { "L" -> {
segments += points!!.map { segments += points!!.map {
Segment(cursor, it).apply { Segment2D(cursor, it).apply {
cursor = it cursor = it
} }
} }
} }
"l" -> { "l" -> {
segments += points!!.map { segments += points!!.map {
Segment(cursor, cursor + it).apply { Segment2D(cursor, cursor + it).apply {
cursor += it cursor += it
} }
} }
@@ -241,7 +241,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"H" -> { "H" -> {
segments += command.operands.map { segments += command.operands.map {
val target = Vector2(it, cursor.y) val target = Vector2(it, cursor.y)
Segment(cursor, target).apply { Segment2D(cursor, target).apply {
cursor = target cursor = target
} }
} }
@@ -249,7 +249,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"h" -> { "h" -> {
segments += command.operands.map { segments += command.operands.map {
val target = cursor + Vector2(it, 0.0) val target = cursor + Vector2(it, 0.0)
Segment(cursor, target).apply { Segment2D(cursor, target).apply {
cursor = target cursor = target
} }
} }
@@ -257,7 +257,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"V" -> { "V" -> {
segments += command.operands.map { segments += command.operands.map {
val target = Vector2(cursor.x, it) val target = Vector2(cursor.x, it)
Segment(cursor, target).apply { Segment2D(cursor, target).apply {
cursor = target cursor = target
} }
} }
@@ -265,7 +265,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"v" -> { "v" -> {
segments += command.operands.map { segments += command.operands.map {
val target = cursor + Vector2(0.0, it) val target = cursor + Vector2(0.0, it)
Segment(cursor, target).apply { Segment2D(cursor, target).apply {
cursor = target cursor = target
} }
} }
@@ -277,7 +277,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach return@forEach
} else { } else {
val (cp1, cp2, target) = it val (cp1, cp2, target) = it
Segment(cursor, cp1, cp2, target).also { Segment2D(cursor, cp1, cp2, target).also {
cursor = target cursor = target
prevCubicCtrlPoint = cp2 prevCubicCtrlPoint = cp2
} }
@@ -291,7 +291,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach return@forEach
} else { } else {
val (cp1, cp2, target) = it.map { v -> cursor + v } val (cp1, cp2, target) = it.map { v -> cursor + v }
Segment(cursor, cp1, cp2, target).apply { Segment2D(cursor, cp1, cp2, target).apply {
cursor = target cursor = target
prevCubicCtrlPoint = cp2 prevCubicCtrlPoint = cp2
} }
@@ -306,7 +306,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
} else { } else {
val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor) val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
val (cp2, target) = it val (cp2, target) = it
Segment(cursor, cp1, cp2, target).also { Segment2D(cursor, cp1, cp2, target).also {
cursor = target cursor = target
prevCubicCtrlPoint = cp2 prevCubicCtrlPoint = cp2
} }
@@ -321,7 +321,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
} else { } else {
val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor) val cp1 = 2.0 * cursor - (prevCubicCtrlPoint ?: cursor)
val (cp2, target) = it.map { v -> cursor + v } val (cp2, target) = it.map { v -> cursor + v }
Segment(cursor, cp1, cp2, target).also { Segment2D(cursor, cp1, cp2, target).also {
cursor = target cursor = target
prevCubicCtrlPoint = cp2 prevCubicCtrlPoint = cp2
} }
@@ -335,7 +335,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach return@forEach
} else { } else {
val (cp, target) = it val (cp, target) = it
Segment(cursor, cp, target).also { Segment2D(cursor, cp, target).also {
cursor = target cursor = target
prevQuadCtrlPoint = cp prevQuadCtrlPoint = cp
} }
@@ -349,7 +349,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
return@forEach return@forEach
} else { } else {
val (cp, target) = it.map { v -> cursor + v } val (cp, target) = it.map { v -> cursor + v }
Segment(cursor, cp, target).also { Segment2D(cursor, cp, target).also {
cursor = target cursor = target
prevQuadCtrlPoint = cp prevQuadCtrlPoint = cp
} }
@@ -359,7 +359,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"T" -> { "T" -> {
points!!.forEach { points!!.forEach {
val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor) val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
Segment(cursor, cp, it).also { _ -> Segment2D(cursor, cp, it).also { _ ->
cursor = it cursor = it
prevQuadCtrlPoint = cp prevQuadCtrlPoint = cp
} }
@@ -368,7 +368,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
"t" -> { "t" -> {
points!!.forEach { points!!.forEach {
val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor) val cp = 2.0 * cursor - (prevQuadCtrlPoint ?: cursor)
Segment(cursor, cp, cursor + it).also { _ -> Segment2D(cursor, cp, cursor + it).also { _ ->
cursor = it cursor = it
prevQuadCtrlPoint = cp prevQuadCtrlPoint = cp
} }
@@ -376,7 +376,7 @@ internal class SVGPath(val element: Element? = null) : SVGElement(element) {
} }
"Z", "z" -> { "Z", "z" -> {
if ((cursor - anchor).length >= 0.001) { if ((cursor - anchor).length >= 0.001) {
segments += Segment(cursor, anchor) segments += Segment2D(cursor, anchor)
} }
cursor = anchor cursor = anchor
closed = true closed = true

View File

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

View File

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

View File

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