diff --git a/orx-shapes/src/main/kotlin/operators/BulgeContours.kt b/orx-shapes/src/main/kotlin/operators/BulgeContours.kt new file mode 100644 index 00000000..e772e902 --- /dev/null +++ b/orx-shapes/src/main/kotlin/operators/BulgeContours.kt @@ -0,0 +1,41 @@ +package org.openrndr.extra.shapes.operators + +import org.openrndr.math.mod_ +import org.openrndr.shape.Segment +import org.openrndr.shape.ShapeContour +import org.openrndr.shape.contour + +fun ShapeContour.bulgeSegments(distortion: (index: Int, segment: Segment) -> Double): ShapeContour { + val c = contour { + moveTo(position(0.0)) + var index = 0 + for (segment in this@bulgeSegments.segments) { + when { + segment.linear -> { + val q = segment.quadratic + + val d = distortion(index, segment) + + curveTo(q.control[0] + segment.normal(0.5) * d, q.end) + index++ + } + segment.control.size == 2 -> { + curveTo(segment.control[0], segment.control[1], segment.end) + } + segment.control.size == 1 -> { + curveTo(segment.control[0], segment.end) + } + } + } + if (this@bulgeSegments.closed) { + close() + } + } + return c +} + +fun ShapeContour.bulgeSegments(distortion: Double) = + bulgeSegments { _, _ -> distortion } + +fun ShapeContour.bulgeSegments(distortion: List) = + bulgeSegments { index, _ -> distortion[index.mod_(distortion.size)] } \ No newline at end of file diff --git a/orx-shapes/src/main/kotlin/operators/ChamferCorners.kt b/orx-shapes/src/main/kotlin/operators/ChamferCorners.kt index dffbcf70..0bae2b21 100644 --- a/orx-shapes/src/main/kotlin/operators/ChamferCorners.kt +++ b/orx-shapes/src/main/kotlin/operators/ChamferCorners.kt @@ -1,6 +1,7 @@ package org.openrndr.extra.shapes.operators import org.openrndr.math.Vector2 +import org.openrndr.math.mod_ import org.openrndr.shape.* import kotlin.math.abs import kotlin.math.sign @@ -35,92 +36,137 @@ private fun pickLength(leftLength: Double, rightLength: Double, s0: Segment, s1: * @param chamfer the chamfer function to apply */ fun ShapeContour.chamferCorners( - leftLength: Double, - rightLength: Double = leftLength, + lengths: (index: Int, left: Segment, right: Segment) -> Double, + expands: (index: Int, left: Segment, right: Segment) -> Double = { _, _, _ -> 0.0 }, clip: Boolean = true, 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 +): ShapeContour { + + if (segments.size <= 1) { + return this } - // Prelude - if ((this@chamferCorners).closed && sourceSegments[sourceSegments.size - 2].linear && sourceSegments.first().linear) { - val length = pickLength(leftLength, rightLength, sourceSegments.last(), sourceSegments.first()) - if (clip || length <= sourceSegments[0].length / 2) { - moveTo(sourceSegments[0].linearPosition(length)) + return contour { + val sourceSegments = if (closed) { + (this@chamferCorners.segments + this@chamferCorners.segments.first()) } else { - moveTo(sourceSegments[0].position(0.0)) + this@chamferCorners.segments } - } 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 && (clip || (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) + var lengthIndex = sourceSegments.size - 1 + + + sourceSegments.first().let { + if (it.control.size == 1) { + moveTo(position(0.0)) + } + if (it.control.size == 2) { + moveTo(position(0.0)) + } + if (it.linear) { + if (!this@chamferCorners.closed) + moveTo(position(0.0)) } } - } - // Postlude - if (closed) { - close() - } else { - val last = sourceSegments.last() - when { - last.linear -> { - if (clip || length <= last.length / 2) { - lineTo(last.linearPosition(length)) + + lengthIndex = 0 + for ((s0, s1) in sourceSegments.zipWithNext()) { + lengthIndex++ + if (s0.control.size == 1) { + moveOrCurveTo(s0.control[0], s0.end) + } else if (s0.control.size == 2) { + moveOrCurveTo(s0.control[0], s0.control[1], s0.end) + } else if (s0.linear) { + + val length = lengths(lengthIndex, s0, s1) + + if (s0.linear && s1.linear && (clip || (length <= s0.length / 2 && length <= s1.length / 2))) { + + val expand = expands(lengthIndex, s0, s1) + + val p0 = s0.linearPosition(s0.length - length) + val p1 = s1.linearPosition(length) + + val d = p1 - p0 + + val q0 = p0 - d * expand + val q1 = p1 + d * expand + + moveOrLineTo(q0) + chamfer(q0, s0.end, q1) } else { - lineTo(last.end) + lineTo(s0.end) } } - last.control.size == 1 -> { - curveTo(last.control[0], last.end) - } - last.control.size == 2 -> { - curveTo(last.control[0], last.control[1], last.end) + } + + // Postlude + if (closed) { + close() + } else { + val last = sourceSegments.last() + when { + last.linear -> { + 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 = angleThreshold) { _, _, p3 -> + chamferCorners({ _, _, _ -> length }, angleThreshold = angleThreshold) { _, _, p3 -> lineTo(p3) } fun ShapeContour.roundCorners(length: Double, angleThreshold: Double = 180.0): ShapeContour = - chamferCorners(length, length, angleThreshold = angleThreshold) { _, p2, p3 -> + chamferCorners({ _, _, _ -> length }, angleThreshold = 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 = 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) - } \ No newline at end of file +fun ShapeContour.arcCorners(lengths: List, + expands: List = listOf(0.0), + scales: List = listOf(1.0), + largeArcs: List = mutableListOf(false), + angleThreshold: Double = 180.0): ShapeContour { + + + val scaleRing = scales.ring() + val lengthRing = lengths.ring() + val expandRing = expands.ring() + val largeArcRing = largeArcs.ring() + + var segmentIndex = 0 + return chamferCorners({ index, _, _ -> lengthRing[index] }, + { index, _, _ -> expandRing[index] }, + angleThreshold = 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 = scaleRing[segmentIndex] + val sweep = scale * sign(det) + val largeArc = largeArcRing[segmentIndex] + arcTo(radius * abs(scale), radius * abs(scale), 90.0, largeArc, sweep > 0.0, p3) + segmentIndex++ + } +} + +private class Ring(private val x: List) : List by x { + override operator fun get(index: Int): T { + return x[index.mod_(x.size)] + } +} + +private fun List.ring(): List { + return Ring(this) +}