Add corner chamfers to orx-shapes
This commit is contained in:
125
orx-shapes/src/main/kotlin/operators/ChamferCorners.kt
Normal file
125
orx-shapes/src/main/kotlin/operators/ChamferCorners.kt
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package org.openrndr.extra.shapes.operators
|
||||||
|
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.shape.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.sign
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
private fun Segment.linearSub(l0: Double, l1: Double): Segment {
|
||||||
|
return sub(l0 / length, l1 / length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Segment.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 {
|
||||||
|
val p3 = s1.end
|
||||||
|
val p2 = s0.end
|
||||||
|
val p1 = s0.start
|
||||||
|
|
||||||
|
val det = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)
|
||||||
|
|
||||||
|
return if (det < 0.0) {
|
||||||
|
leftLength
|
||||||
|
} else {
|
||||||
|
rightLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chamfers corners between linear segments
|
||||||
|
* @param length the length of the chamfer
|
||||||
|
* @param angleThreshold the maximum (smallest) angle between between linear segments
|
||||||
|
* @param chamfer the chamfer function to apply
|
||||||
|
*/
|
||||||
|
fun ShapeContour.chamferCorners(
|
||||||
|
leftLength: Double,
|
||||||
|
rightLength: Double = leftLength,
|
||||||
|
angleThreshold: Double = 180.0,
|
||||||
|
chamfer: ContourBuilder.(p1: Vector2, p2: Vector2, p3: Vector2) -> Unit
|
||||||
|
) = contour {
|
||||||
|
val sourceSegments = if (closed) {
|
||||||
|
(this@chamferCorners.segments + this@chamferCorners.segments.first())
|
||||||
|
} else {
|
||||||
|
this@chamferCorners.segments
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prelude
|
||||||
|
if ((this@chamferCorners).closed && sourceSegments[sourceSegments.size - 2].linear && sourceSegments.first().linear) {
|
||||||
|
val length = pickLength(leftLength, rightLength, sourceSegments.last(), sourceSegments.first())
|
||||||
|
if (length <= sourceSegments[0].length / 2) {
|
||||||
|
moveTo(sourceSegments[0].linearPosition(length))
|
||||||
|
} else {
|
||||||
|
moveTo(sourceSegments[0].position(0.0))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
moveTo(position(0.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((s0, s1) in sourceSegments.zipWithNext()) {
|
||||||
|
if (s0.control.size == 1) {
|
||||||
|
curveTo(s0.control[0], s0.end)
|
||||||
|
} else if (s0.control.size == 2) {
|
||||||
|
curveTo(s0.control[0], s0.control[1], s0.end)
|
||||||
|
} else if (s0.linear) {
|
||||||
|
val length = pickLength(leftLength, rightLength, s0, s1)
|
||||||
|
if (s0.linear && s1.linear && length <= s0.length / 2 && length <= s1.length / 2) {
|
||||||
|
val p0 = s0.linearPosition(s0.length - length)
|
||||||
|
val p1 = s1.linearPosition(length)
|
||||||
|
lineTo(p0)
|
||||||
|
chamfer(p0, s0.end, p1)
|
||||||
|
} else {
|
||||||
|
lineTo(s0.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Postlude
|
||||||
|
if (closed) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
val last = sourceSegments.last()
|
||||||
|
when {
|
||||||
|
last.linear -> {
|
||||||
|
if (length <= last.length / 2) {
|
||||||
|
lineTo(last.linearPosition(length))
|
||||||
|
} else {
|
||||||
|
lineTo(last.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last.control.size == 1 -> {
|
||||||
|
curveTo(last.control[0], last.end)
|
||||||
|
}
|
||||||
|
last.control.size == 2 -> {
|
||||||
|
curveTo(last.control[0], last.control[1], last.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ShapeContour.bevelCorners(length: Double, angleThreshold: Double = 180.0): ShapeContour =
|
||||||
|
chamferCorners(length, length, angleThreshold) { _, _, p3 ->
|
||||||
|
lineTo(p3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ShapeContour.roundCorners(length: Double, angleThreshold: Double = 180.0): ShapeContour =
|
||||||
|
chamferCorners(length, length, angleThreshold) { _, p2, p3 ->
|
||||||
|
curveTo(p2, p3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ShapeContour.arcCorners(leftLength: Double, rightLength: Double = leftLength,
|
||||||
|
leftScale: Double = 1.0, rightScale: Double = leftScale,
|
||||||
|
leftLargeArc : Boolean = false, rightLargeArc : Boolean = leftLargeArc,
|
||||||
|
angleThreshold: Double = 180.0): ShapeContour =
|
||||||
|
chamferCorners(abs(leftLength), abs(rightLength), angleThreshold) { p1, p2, p3 ->
|
||||||
|
val dx = abs(p3.x - p2.x)
|
||||||
|
val dy = abs(p3.y - p2.y)
|
||||||
|
val radius = sqrt(dx * dx + dy * dy)
|
||||||
|
val det = (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)
|
||||||
|
val scale = if (det < 0.0) leftScale else rightScale
|
||||||
|
val sweep = scale * sign(det)
|
||||||
|
val largeArc = if (det < 0.0) leftLargeArc else rightLargeArc
|
||||||
|
arcTo(radius * abs(scale) , radius * abs(scale), 90.0, largeArc, sweep > 0.0, p3)
|
||||||
|
}
|
||||||
7
orx-shapes/src/test/kotlin/Assertions.kt
Normal file
7
orx-shapes/src/test/kotlin/Assertions.kt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import org.amshove.kluent.shouldBeInRange
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
infix fun Vector2.`should be near`(other: Vector2) {
|
||||||
|
x shouldBeInRange (other.x - 0.00001..other.x + 0.00001)
|
||||||
|
y shouldBeInRange (other.y - 0.00001..other.y + 0.00001)
|
||||||
|
}
|
||||||
153
orx-shapes/src/test/kotlin/TestChamferCorners.kt
Normal file
153
orx-shapes/src/test/kotlin/TestChamferCorners.kt
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import org.amshove.kluent.`should be equal to`
|
||||||
|
import org.openrndr.extra.shapes.operators.bevelCorners
|
||||||
|
import org.openrndr.extra.shapes.operators.roundCorners
|
||||||
|
import org.openrndr.extra.shapes.regularPolygon
|
||||||
|
import org.openrndr.shape.Circle
|
||||||
|
import org.openrndr.shape.contour
|
||||||
|
import org.spekframework.spek2.Spek
|
||||||
|
import org.spekframework.spek2.style.specification.describe
|
||||||
|
|
||||||
|
object TestChamferCorners : Spek({
|
||||||
|
|
||||||
|
describe("a single segment linear contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
lineTo(100.0, 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be similar to a chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` 1
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a single segment quadratic contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
curveTo(40.0, 40.0, 100.0, 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should be similar to a chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` 1
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(0.5) `should be near` c.position(0.5)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a circle contour") {
|
||||||
|
val c = Circle(0.0, 0.0, 200.0).contour
|
||||||
|
|
||||||
|
it("should be similar to a chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` c.segments.size
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(0.5) `should be near` c.position(0.5)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a two segment linear contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
lineTo(50.0, 50.0)
|
||||||
|
lineTo(100.0, 50.0)
|
||||||
|
}
|
||||||
|
it("should chamfer correctly") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` 3
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a two segment linear-curve contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
lineTo(50.0, 50.0)
|
||||||
|
curveTo(80.0, 120.0, 100.0, 50.0)
|
||||||
|
}
|
||||||
|
it("should be identical to the chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` c.segments.size
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a two segment curve-linear contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
curveTo(80.0, 120.0, 50.0, 50.0)
|
||||||
|
lineTo(100.0, 50.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
it("should be identical to the chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` c.segments.size
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a two segment curve-linear contour") {
|
||||||
|
val c = contour {
|
||||||
|
moveTo(0.0, 0.0)
|
||||||
|
curveTo(80.0, 120.0, 50.0, 50.0)
|
||||||
|
lineTo(100.0, 50.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
it("should be identical to the chamfered version") {
|
||||||
|
val cc = c.bevelCorners(10.0)
|
||||||
|
cc.segments.size `should be equal to` c.segments.size
|
||||||
|
cc.position(0.0) `should be near` c.position(0.0)
|
||||||
|
cc.position(1.0) `should be near` c.position(1.0)
|
||||||
|
cc.closed `should be equal to` c.closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("a triangle") {
|
||||||
|
val c = regularPolygon(3, radius = 100.0)
|
||||||
|
|
||||||
|
c.closed `should be equal to` true
|
||||||
|
|
||||||
|
val cc = c.roundCorners(1.0)
|
||||||
|
|
||||||
|
c.closed `should be equal to` cc.closed
|
||||||
|
|
||||||
|
val ccc = cc.roundCorners(1.0)
|
||||||
|
|
||||||
|
ccc.closed `should be equal to` cc.closed
|
||||||
|
|
||||||
|
cc.segments.size `should be equal to` 6
|
||||||
|
|
||||||
|
cc.segments.forEach {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
println("---")
|
||||||
|
ccc.segments.forEach {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
it("should have 6 sides") {
|
||||||
|
ccc.segments.size `should be equal to` cc.segments.size
|
||||||
|
}
|
||||||
|
it("should start at the right position") {
|
||||||
|
ccc.position(0.0) `should be near` cc.position(0.0)
|
||||||
|
}
|
||||||
|
it("should end at the right position") {
|
||||||
|
ccc.position(1.0) `should be near` cc.position(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user