From 2e6c637b49dc85489baee1414681b03535211fdd Mon Sep 17 00:00:00 2001 From: Abe Pazos Date: Fri, 29 Aug 2025 13:00:27 +0200 Subject: [PATCH] [orx-shapes] Comment the ContourAdjuster and its demos --- .../kotlin/adjust/ContourAdjusterEdge.kt | 86 +++++++++++++++++-- .../commonMain/kotlin/adjust/ContourEdge.kt | 74 ++++++++++++---- .../kotlin/adjust/DemoAdjustContour04.kt | 2 +- .../kotlin/adjust/DemoAdjustContour05.kt | 13 +++ .../kotlin/adjust/DemoAdjustContour06.kt | 18 +++- .../kotlin/adjust/DemoAdjustContour07.kt | 11 ++- .../kotlin/adjust/DemoAdjustContour08.kt | 13 ++- .../kotlin/adjust/DemoAdjustContour09.kt | 40 +++++++-- .../adjust/DemoAdjustContourContinue01.kt | 24 +++++- 9 files changed, 248 insertions(+), 33 deletions(-) diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt index 196b2e35..9ce4cdd7 100644 --- a/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourAdjusterEdge.kt @@ -79,41 +79,113 @@ data class ContourAdjusterEdge(val contourAdjuster: ContourAdjuster, val segment contourAdjuster.updateSelection(newEdge.adjustments) } + /** + * Convert the edge to a linear edge, truncating control points if those exist + */ fun toLinear() = wrap { toLinear() } + /** + * Convert the edge to a cubic edge + */ fun toCubic() = wrap { toCubic() } + + /** + * Split the edge at [t] + * @param t an edge t value between 0 and 1. No splitting happens when t == 0 or t == 1. + */ fun splitAt(t: Double) = wrap { splitAt(t) } /** - * split edge in [numberOfParts] parts of equal length + * Split the edge in [numberOfParts] parts of equal length */ fun splitIn(numberOfParts: Int) = wrap { splitIn(numberOfParts) } + /** + * Creates a new contour edge by applying a translation to the current edge. + * + * @param translation the translation vector to apply to the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. + * @return a new instance of the contour edge, transformed by the given translation. + */ fun moveBy(translation: Vector2, updateTangents: Boolean = true) = wrap { movedBy(translation, updateTangents) } + + /** + * Rotates the current edge by a specified angle around an anchor point relative to the edge. + * Optionally updates the tangents of adjacent segments after the rotation. + * + * @param rotationInDegrees the rotation angle in degrees to be applied. + * @param anchorT the relative position along the edge (range 0.0 to 1.0) defining the anchor point of rotation. Defaults to 0.5 (the center of the edge). + * @param updateTangents whether the tangents of adjacent segments should be updated after the rotation. Defaults to true. + */ fun rotate(rotationInDegrees: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) = wrap { rotatedBy(rotationInDegrees, anchorT, updateTangents) } - fun scale(scaleFactor: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) = + /** + * Scales the current edge by a specified factor, with an optional anchor point determining the + * scaling center along the edge. The scaling operation updates the tangents of the edge. + */ + fun scale(scaleFactor: Double, anchorT: Double = 0.5) = wrap { scaledBy(scaleFactor, anchorT, updateTangents = true) } - fun replaceWith(t: Double, updateTangents: Boolean = true) = wrap { replacedWith(t, updateTangents) } + /** + * Replace this edge with a point at [t] + * @param t an edge t value between 0 and 1 + */ + fun replaceWith(t: Double) = wrap { replacedWith(t) } + /** + * Replaces the current edge with the segments of an open shape contour. + * + * @param openContour the open shape contour whose segments replace the current edge. The provided + * contour must not be closed. + * @return a new ContourEdge instance with the updated segments from the `openContour`. + */ fun replaceWith(openContour: ShapeContour) = wrap { replacedWith(openContour) } - - fun sub(t0: Double, t1: Double, updateTangents: Boolean = true) { + /** + * Returns part of the edge between [t0] to [t1]. + * Preserves topology unless t0 = t1. + * @param t0 the edge's start t-value, between 0 and 1 + * @param t1 the edge's end t-value, between 0 and 1 + */ + fun sub(t0: Double, t1: Double) { contourAdjuster.contour = ContourEdge(contourAdjuster.contour, segmentIndex()) .subbed(t0, t1) .contour } + /** + * Moves the starting point of the contour edge by the given translation vector. + * + * @param translation the translation vector to apply to the starting point of the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true. + * @return a new instance of the contour edge with the starting point moved by the given translation. + */ fun moveStartBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) } + /** + * Moves the first control point of a contour edge by a specified translation vector. + * + * @param translation the translation vector to apply to the first control point of the contour edge. + * @return a new instance of the contour edge with the first control point moved by the given translation. + */ fun moveControl0By(translation: Vector2) = wrap { control0MovedBy(translation) } + /** + * Moves the second control point (Control1) of a contour edge by a specified translation vector. + * + * @param translation the translation vector to apply to the second control point of the contour edge. + * @return a new instance of the contour edge with the second control point moved by the given translation. + */ fun moveControl1By(translation: Vector2) = wrap { control1MovedBy(translation) } + + /** + * Moves the end point of the contour edge by the specified translation vector. + * + * @param translation the translation vector to apply to the end point of the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true. + * @return a new instance of the contour edge with the end point moved by the given translation. + */ fun moveEndBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) } - - } diff --git a/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt b/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt index d8971b23..3eeac964 100644 --- a/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt +++ b/orx-shapes/src/commonMain/kotlin/adjust/ContourEdge.kt @@ -57,7 +57,7 @@ data class ContourEdge( } /** - * convert the edge to a linear edge, truncating control points if those exist + * Convert the edge to a linear edge, truncating control points if those exist */ fun toLinear(): ContourEdge { return if (contour.segments[segmentIndex].type != SegmentType.LINEAR) { @@ -75,7 +75,7 @@ data class ContourEdge( } /** - * convert the edge to a cubic edge + * Convert the edge to a cubic edge */ fun toCubic(): ContourEdge { return if (contour.segments[segmentIndex].type != SegmentType.CUBIC) { @@ -99,10 +99,10 @@ data class ContourEdge( /** - * replace this edge with a point at [t] + * Replace this edge with a point at [t] * @param t an edge t value between 0 and 1 */ - fun replacedWith(t: Double, updateTangents: Boolean): ContourEdge { + fun replacedWith(t: Double): ContourEdge { if (contour.empty) { return withoutAdjustments() } @@ -125,6 +125,9 @@ data class ContourEdge( return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments) } + /** + * Split the edge in [numberOfParts] parts of equal length + */ fun splitIn(parts: Int): ContourEdge { if (contour.empty || parts < 2) { return withoutAdjustments() @@ -140,13 +143,20 @@ data class ContourEdge( return replacedWith(ShapeContour.fromContours(newSegments, false, 1.0)) } + /** + * Replaces the current edge with the segments of an open shape contour. + * + * @param openContour the open shape contour whose segments replace the current edge. The provided + * contour must not be closed. + * @return a new ContourEdge instance with the updated segments from the `openContour`. + */ fun replacedWith(openContour: ShapeContour): ContourEdge { if (contour.empty) { return withoutAdjustments() } require(!openContour.closed) { "openContour should be open" } val segment = contour.segments[segmentIndex] - var newSegments = contour.segments.toMutableList() + val newSegments = contour.segments.toMutableList() var insertIndex = segmentIndex val adjustments = newSegments.adjust { @@ -169,11 +179,12 @@ data class ContourEdge( /** - * subs the edge from [t0] to [t1], preserves topology unless t0 = t1 - * @param t0 the start edge t-value, between 0 and 1 - * @param t1 the end edge t-value, between 0 and 1 + * Returns part of the edge between [t0] to [t1]. + * Preserves topology unless t0 = t1. + * @param t0 the edge's start t-value, between 0 and 1 + * @param t1 the edge's end t-value, between 0 and 1 */ - fun subbed(t0: Double, t1: Double, updateTangents: Boolean = true): ContourEdge { + fun subbed(t0: Double, t1: Double): ContourEdge { if (contour.empty) { return withoutAdjustments() } @@ -195,23 +206,23 @@ data class ContourEdge( newSegments[segmentIndex] = sub return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex) } else { - return replacedWith(t0, updateTangents) + return replacedWith(t0) } } /** - * split the edge at [t] - * @param t an edge t value between 0 and 1, will not split when t == 0 or t == 1 + * Split the edge at [t] + * @param t An edge t value between 0 and 1. No splitting happens when t == 0 or t == 1. */ fun splitAt(t: Double): ContourEdge { if (contour.empty) { return withoutAdjustments() } val newContour = contour.insertPointAt(segmentIndex, t) - if (newContour.segments.size == contour.segments.size + 1) { - return ContourEdge(newContour, segmentIndex, listOf(SegmentOperation.Insert(segmentIndex + 1, 1))) + return if (newContour.segments.size == contour.segments.size + 1) { + ContourEdge(newContour, segmentIndex, listOf(SegmentOperation.Insert(segmentIndex + 1, 1))) } else { - return this.copy(adjustments = emptyList()) + this.copy(adjustments = emptyList()) } } @@ -282,25 +293,58 @@ data class ContourEdge( return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex) } + /** + * Moves the starting point of the contour edge by the given translation vector. + * + * @param translation the translation vector to apply to the starting point of the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true. + * @return a new instance of the contour edge with the starting point moved by the given translation. + */ fun startMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge = transformedBy(buildTransform { translate(translation) }, updateTangents = updateTangents, mask = maskOf(ControlMask.START)) + /** + * Moves the first control point of a contour edge by a specified translation vector. + * + * @param translation the translation vector to apply to the first control point of the contour edge. + * @return a new instance of the contour edge with the first control point moved by the given translation. + */ fun control0MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform { translate(translation) }, updateTangents = false, mask = maskOf(ControlMask.CONTROL0), promoteToCubic = true) + /** + * Moves the second control point (Control1) of a contour edge by a specified translation vector. + * + * @param translation the translation vector to apply to the second control point of the contour edge. + * @return a new instance of the contour edge with the second control point moved by the given translation. + */ fun control1MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform { translate(translation) }, updateTangents = false, mask = maskOf(ControlMask.CONTROL1), promoteToCubic = true) + /** + * Moves the end point of the contour edge by the specified translation vector. + * + * @param translation the translation vector to apply to the end point of the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true. + * @return a new instance of the contour edge with the end point moved by the given translation. + */ fun endMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge { return transformedBy(buildTransform { translate(translation) }, updateTangents = updateTangents, mask = maskOf(ControlMask.END)) } + /** + * Creates a new contour edge by applying a translation to the current edge. + * + * @param translation the translation vector to apply to the contour edge. + * @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. + * @return a new instance of the contour edge, transformed by the given translation. + */ fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge { return transformedBy(buildTransform { translate(translation) diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour04.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour04.kt index fc44503d..0deef5cd 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour04.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour04.kt @@ -9,7 +9,7 @@ import kotlin.math.cos /** * Demonstrates an `adjustContour` animated effect where edge 0 of a contour * is replaced by a point sampled on that edge. The specific edge point oscillates between - * 0.0 (at the start) and 1.0 (at the end) using a cosine and the `seconds` variable. + * 0.0 (at the start of the segment) and 1.0 (at the end) using a cosine and the `seconds` variable. * * The base contour used for the effect alternates every second * between a rectangular and a circular contour. diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour05.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour05.kt index 59e1439f..5ec0f9cf 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour05.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour05.kt @@ -6,6 +6,19 @@ import org.openrndr.extra.shapes.adjust.adjustContour import org.openrndr.shape.Circle import kotlin.math.cos +/** + * Demonstrates animated modifications to a circular contour using `adjustContour`. + * + * The application creates a circular contour and dynamically alters its edges + * based on the current time in seconds. Each edge of the contour is selected + * and transformed through a series of operations: + * + * - The currently active edge (based on time modulo 4) is replaced with a point at 0.5. + * - All other edges are reshaped by reducing their length dynamically, with the reduction + * calculated using a cosine function involving the current time in seconds. + * + * The resulting contour is then drawn with a red stroke color. + */ fun main() = application { configure { width = 800 diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour06.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour06.kt index c0dd3f82..22cab587 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour06.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour06.kt @@ -6,6 +6,22 @@ import org.openrndr.extra.shapes.adjust.adjustContour import org.openrndr.shape.Circle import kotlin.math.cos +/** + * Demonstrates the use of `adjustContour` + * to create an animated effect where edges are split, vertices are selected, + * and transformations such as scaling are applied. + * + * The program creates a circular contour which is modified on each animation frame. + * + * - Edges of the circular contour are split dynamically based on a time-based cosine function. + * - Newly created vertices are selected and scaled around the center of the contour + * using time-dependent transformations. + * + * The selection of vertices happens automatically thanks to + * `parameters.clearSelectedVertices` and `parameters.selectInsertedVertices` + * + * The modified animated contour is finally drawn. + */ fun main() = application { configure { width = 800 @@ -27,7 +43,7 @@ fun main() = application { for (e in edges) { e.splitAt(splitT) } - // as a resut of the clearSelectedVertices and selectInsertedVertices settings + // as a result of the clearSelectedVertices and selectInsertedVertices settings, // the vertex selection is set to the newly inserted vertices for ((index, v) in vertices.withIndex()) { v.scale(cos(seconds + i + index) * 0.5 * (1.0 / (1.0 + i)) + 1.0, drawer.bounds.center) diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour07.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour07.kt index 4a7b19d4..b1f24dd1 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour07.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour07.kt @@ -7,6 +7,15 @@ import org.openrndr.math.Vector2 import org.openrndr.shape.contour import kotlin.math.cos +/** + * Demonstrates how to create and manipulate a contour dynamically using the `adjustContour` function. + * + * The program initializes a simple linear contour and applies transformations to it on each animation frame: + * - The only edge of the contour is split into many equal parts. + * - A value between 0 and 1 is calculated based on the cosine of the current time in seconds. + * - That value is used to calculate an anchor point and to select all vertices to its right + * - The selected vertices are rotated around an anchor, as if rolling a straight line into a spiral. + */ fun main() = application { configure { width = 800 @@ -22,7 +31,7 @@ fun main() = application { contour = adjustContour(contour) { selectEdge(0) edge.splitIn(128) - val tr = cos(seconds) * 0.5 + 0.5 + val tr = cos(seconds + 2.0) * 0.5 + 0.5 selectVertices { i, v -> v.t >= tr } val anchor = contour.position(tr) diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour08.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour08.kt index 67ba13ba..36657982 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour08.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour08.kt @@ -6,6 +6,16 @@ import org.openrndr.extra.shapes.adjust.adjustContour import org.openrndr.math.Vector2 import org.openrndr.shape.contour +/** + * Demonstrates how to adjust and manipulate the vertices and edges of a contour. + * + * This method shows two approaches for transforming contours: + * + * 1. Adjusting vertices directly by selecting specific vertices in a contour and modifying their control points. + * 2. Adjusting edges of a contour by transforming their control points. + * + * For each approach, a red line is drawn representing the transformed contour. + */ fun main() = application { configure { width = 800 @@ -13,6 +23,7 @@ fun main() = application { } program { extend { + // Adjust a contour by transforming its vertices var contour = contour { moveTo(drawer.bounds.position(0.5, 0.1) - Vector2(300.0, 0.0)) lineTo(drawer.bounds.position(0.5, 0.1) + Vector2(300.0, 0.0)) @@ -29,7 +40,7 @@ fun main() = application { drawer.stroke = ColorRGBa.RED drawer.contour(contour) - + // Achieve the same effect by transforming the control points of its edge contour = contour { moveTo(drawer.bounds.position(0.5, 0.2) - Vector2(300.0, 0.0)) lineTo(drawer.bounds.position(0.5, 0.2) + Vector2(300.0, 0.0)) diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour09.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour09.kt index 6955f415..04f9dd75 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour09.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContour09.kt @@ -2,6 +2,8 @@ package adjust import org.openrndr.application import org.openrndr.color.ColorRGBa +import org.openrndr.draw.loadFont +import org.openrndr.extra.color.presets.DARK_CYAN import org.openrndr.extra.shapes.adjust.adjustContour import org.openrndr.extra.shapes.adjust.extensions.averageTangents import org.openrndr.extra.shapes.adjust.extensions.switchTangents @@ -9,14 +11,36 @@ import org.openrndr.extra.shapes.tunni.tunniLine import org.openrndr.extra.shapes.tunni.tunniPoint import kotlin.math.sqrt +/** + * Demonstrates how to manipulate a contour by adjusting and transforming its vertices + * and edges, and subsequently visualizing the result using different drawing styles. + * + * The program creates a rectangular contour derived by shrinking the bounds of the drawing area. + * It then applies multiple transformations to selected vertices. These transformations include: + * + * - Averaging tangents for selected vertices + * - Scaling and rotating vertex positions based on the horizontal mouse position + * - Switching tangents for specific vertices + * + * The resulting contour is drawn in black. Additionally: + * + * - Control line segments are visualized in red, connecting segment endpoints to control points. + * - Vertices are numbered and highlighted with black-filled circles. + * - Tunni lines, which represent optimized control line placements, are visualized in cyan. + * - Tunni points, marking the Tunni line's control, are emphasized with yellow-filled circles. + * + */ fun main() = application { configure { width = 800 height = 800 } program { + val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0) extend { drawer.clear(ColorRGBa.WHITE) + drawer.fontMap = font + var contour = drawer.bounds.offsetEdges(-200.0).contour drawer.fill = null @@ -26,7 +50,7 @@ fun main() = application { for (v in vertices) { v.averageTangents() v.scale(sqrt(2.0)) - v.rotate(45.0) + v.rotate(mouse.position.x - 45.0) } selectVertices(2) @@ -38,19 +62,25 @@ fun main() = application { drawer.contour(contour) drawer.stroke = ColorRGBa.RED - for (s in contour.segments) { drawer.lineSegment(s.start, s.cubic.control[0]) drawer.lineSegment(s.end, s.cubic.control[1]) } + + // Draw points and numbers drawer.fill = ColorRGBa.BLACK drawer.stroke = null - drawer.circles(contour.segments.map { it.start }, 5.0) + contour.segments.forEachIndexed { i, it -> + drawer.text(i.toString(), it.start + 10.0) + drawer.circle(it.start, 5.0) + } - drawer.stroke = ColorRGBa.GRAY + drawer.fill = ColorRGBa.YELLOW + drawer.stroke = ColorRGBa.CYAN for (s in contour.segments) { + drawer.strokeWeight = 3.0 drawer.lineSegment(s.tunniLine) - drawer.fill = ColorRGBa.CYAN + drawer.strokeWeight = 1.0 drawer.circle(s.tunniPoint, 5.0) } } diff --git a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContourContinue01.kt b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContourContinue01.kt index f47d46e8..dbbfc218 100644 --- a/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContourContinue01.kt +++ b/orx-shapes/src/jvmDemo/kotlin/adjust/DemoAdjustContourContinue01.kt @@ -12,6 +12,24 @@ import org.openrndr.math.Vector2 import org.openrndr.shape.Segment2D import kotlin.math.cos +/** + * Demonstrates how to adjust and animate contour segments and vertices. + * + * The method initially creates a contour by offsetting the edges of the window's bounds. A process is + * defined to sequence through various transformations on the contour, such as selecting edges, selecting + * vertices, rotating points, or modifying segment attributes based on mathematical transformations. + * + * The adjusted contour and its modified segments and vertices are iterated through a sequence + * and updated in real time. Rendering involves visualizing the contour, its control points, the + * Tunni lines, Tunni points, as well as the selected segments and points with distinct styles + * for better visualization. + * + * The complex animation sequence is implemented using coroutines. Two loops in the code alternate + * between rotating vertices and adjusting Tunni lines while the `extend` function takes care of + * rendering the composition in its current state. + * + * The core elements to study to in this demo are `adjustContourSequence` and `launch`. + */ fun main() = application { configure { width = 800 @@ -39,7 +57,6 @@ fun main() = application { selectVertices((i * 3).mod(4)) for (v in vertices) { yield(status) - } @@ -47,7 +64,10 @@ fun main() = application { selectEdges(i.mod(4)) for (j in 0 until 30) { for (e in edges) { - e.withTunniLine(e.tunniLine.position(0.5) + e.tunniLine.normal * cos(i.toDouble() + e.segmentIndex()) * 50.0 / 30.0) + e.withTunniLine( + e.tunniLine.position(0.5) + + e.tunniLine.normal * cos(i.toDouble() + e.segmentIndex()) * 50.0 / 30.0 + ) yield(status) } }