diff --git a/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt b/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt index 31982b11..6686a812 100644 --- a/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt +++ b/orx-marching-squares/src/commonMain/kotlin/MarchingSquares.kt @@ -4,17 +4,28 @@ import org.openrndr.math.IntVector2 import org.openrndr.math.Vector2 import org.openrndr.shape.LineSegment import org.openrndr.shape.Rectangle +import org.openrndr.shape.ShapeContour import kotlin.math.max import kotlin.math.min + +/** + * Find contours for a function [f] using the marching squares algorithm. A contour is found when f(x) crosses zero. + * @param f the function + * @param area a rectangular area in which the function should be evaluated + * @param cellSize the size of the cells, smaller size gives higher resolution + * @param useInterpolation intersection points will be interpolated if true, default true + * @return a list of [ShapeContour] instances + */ fun findContours( f: (Vector2) -> Double, area: Rectangle, cellSize: Double, useInterpolation: Boolean = true -): List { +): List { val segments = mutableListOf() val values = mutableMapOf() + val segmentsMap = mutableMapOf>() for (y in 0 until (area.width / cellSize).toInt()) { for (x in 0 until (area.width / cellSize).toInt()) { @@ -25,9 +36,12 @@ fun findContours( val zero = 0.0 for (y in 0 until (area.width / cellSize).toInt()) { for (x in 0 until (area.width / cellSize).toInt()) { - val v00 = (values[IntVector2(x, y)] ?: zero) - val v10 = (values[IntVector2(x + 1, y)] ?: zero) - val v01 = (values[IntVector2(x, y + 1)] ?: zero) + + // Here we check if we are at a right or top border. This is to ensure we create closed contours + // later on in the process. + val v00 = if (x == 0 || y == 0) zero else (values[IntVector2(x, y)] ?: zero) + val v10 = if (y == 0) zero else (values[IntVector2(x + 1, y)] ?: zero) + val v01 = if (x == 0) zero else (values[IntVector2(x, y + 1)] ?: zero) val v11 = (values[IntVector2(x + 1, y + 1)] ?: zero) val p00 = Vector2(x.toDouble(), y.toDouble()) * cellSize + area.corner @@ -66,7 +80,12 @@ fun findContours( ) { val r0 = blend(v00, v01) val r1 = blend(v10, v11) - val l0 = LineSegment(p00.mix(p01, r0), p10.mix(p11, r1)) + + val v0 = p00.mix(p01, r0) + val v1 = p10.mix(p11, r1) + val l0 = LineSegment(v0, v1) + segmentsMap.getOrPut(v1) { mutableListOf() }.add(l0) + segmentsMap.getOrPut(v0) { mutableListOf() }.add(l0) segments.add(l0) } @@ -103,5 +122,32 @@ fun findContours( } } } - return segments + + val processedSegments = mutableSetOf() + val contours = mutableListOf() + for (segment in segments) { + if (segment in processedSegments) { + continue + } else { + val collected = mutableListOf() + var current: LineSegment? = segment + var closed = true + do { + current!! + collected.add(current.start) + processedSegments.add(current) + if (segmentsMap[current.start]!!.size < 2) { + closed = false + } + val hold = current + current = segmentsMap[current.start]?.firstOrNull { it !in processedSegments } + if (current == null) { + current = segmentsMap[hold.end]?.firstOrNull { it !in processedSegments } + } + } while (current != segment && current != null) + + contours.add(ShapeContour.fromPoints(collected, closed = closed)) + } + } + return contours } \ No newline at end of file diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt index d8354edd..3bb7f909 100644 --- a/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours01.kt @@ -14,8 +14,9 @@ fun main() { drawer.clear(ColorRGBa.BLACK) drawer.stroke = ColorRGBa.PINK fun f(v: Vector2) = v.distanceTo(drawer.bounds.center) - 200.0 - val segments = findContours(::f, drawer.bounds, 16.0) - drawer.lineSegments(segments) + val contours = findContours(::f, drawer.bounds, 16.0) + drawer.fill = null + drawer.contours(contours) } } } diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt index 225d2b15..04a9b68f 100644 --- a/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours02.kt @@ -16,8 +16,9 @@ fun main() { drawer.clear(ColorRGBa.BLACK) drawer.stroke = ColorRGBa.PINK fun f(v: Vector2) = cos((v.distanceTo(drawer.bounds.center) / 100.0) * 2 * PI) - val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 16.0) - drawer.lineSegments(segments) + val contours = findContours(::f, drawer.bounds.offsetEdges(-24.0), 16.0) + drawer.fill = null + drawer.contours(contours) } } } diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt index 0a8746a5..7721b391 100644 --- a/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours03.kt @@ -16,14 +16,13 @@ fun main() { extend { drawer.clear(ColorRGBa.BLACK) drawer.stroke = ColorRGBa.PINK + drawer.fill = null fun f(v: Vector2): Double { - val p = v + Vector2(cos(v.y * 0.1 + seconds) * 40.0, sin(v.x * 0.1 + seconds) * 40.0) - return cos((p.distanceTo(drawer.bounds.center) / 720.0) * 6 * PI) + return cos((p.distanceTo(drawer.bounds.center) / 720.0) * 12 * PI) } - - val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) - drawer.lineSegments(segments) + val contours = findContours(::f, drawer.bounds.offsetEdges(-2.0), 4.0) + drawer.contours(contours) } } } diff --git a/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt b/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt index 5a285219..e19116b4 100644 --- a/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt +++ b/orx-marching-squares/src/jvmDemo/kotlin/FindContours04.kt @@ -6,7 +6,6 @@ import org.openrndr.extra.marchingsquares.findContours import org.openrndr.math.Vector2 import kotlin.math.PI import kotlin.math.cos -import kotlin.math.sin fun main() { application { @@ -20,16 +19,17 @@ fun main() { extend { drawer.clear(ColorRGBa.BLACK) drawer.stroke = ColorRGBa.BLACK + drawer.fill = null fun f(v: Vector2): Double { val iv = v.toInt() val d = if (iv.x >= 0 && iv.y >= 0 && iv.x < image.width && iv.y < image.height) image.shadow[iv.x, iv.y].luminance else 0.0 return cos(d * PI * 8.0 + seconds) } - val segments = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) + val contours = findContours(::f, drawer.bounds.offsetEdges(32.0), 4.0) drawer.drawStyle.colorMatrix = grayscale() drawer.image(image) - drawer.lineSegments(segments) + drawer.contours(contours) } } }