[orx-shapes] Add alpha shapes (#203)
This commit is contained in:
committed by
GitHub
parent
af524b8e42
commit
ee3a3603c0
@@ -26,6 +26,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":orx-camera"))
|
implementation(project(":orx-camera"))
|
||||||
implementation(project(":orx-color"))
|
implementation(project(":orx-color"))
|
||||||
|
implementation(project(":orx-jvm:orx-triangulation"))
|
||||||
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||||
implementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
implementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||||
runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||||
@@ -67,6 +68,12 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":orx-jvm:orx-triangulation"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test-common"))
|
implementation(kotlin("test-common"))
|
||||||
|
|||||||
24
orx-shapes/src/demo/kotlin/DemoAlphaShape.kt
Normal file
24
orx-shapes/src/demo/kotlin/DemoAlphaShape.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.shapes.AlphaShape
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
program {
|
||||||
|
val points = List(20) {
|
||||||
|
Vector2(
|
||||||
|
Random.nextDouble(width*0.25, width*0.75),
|
||||||
|
Random.nextDouble(height*0.25, height*0.75)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val alphaShape = AlphaShape(points)
|
||||||
|
val c = alphaShape.create()
|
||||||
|
extend {
|
||||||
|
drawer.fill = ColorRGBa.PINK
|
||||||
|
drawer.contour(c)
|
||||||
|
drawer.fill = ColorRGBa.WHITE
|
||||||
|
drawer.circles(points, 4.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ import org.openrndr.application
|
|||||||
import org.openrndr.color.ColorRGBa
|
import org.openrndr.color.ColorRGBa
|
||||||
import org.openrndr.draw.loadFont
|
import org.openrndr.draw.loadFont
|
||||||
import org.openrndr.extensions.SingleScreenshot
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extra.color.spaces.toOKLABa
|
||||||
import org.openrndr.extra.shapes.bezierPatch
|
import org.openrndr.extra.shapes.bezierPatch
|
||||||
import org.openrndr.extra.shapes.drawers.bezierPatch
|
import org.openrndr.extra.shapes.drawers.bezierPatch
|
||||||
import org.openrndr.extras.color.spaces.toOKLABa
|
|
||||||
import org.openrndr.shape.Circle
|
import org.openrndr.shape.Circle
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.openrndr.extensions.SingleScreenshot
|
|||||||
import org.openrndr.extra.shapes.bezierPatch
|
import org.openrndr.extra.shapes.bezierPatch
|
||||||
import org.openrndr.extra.shapes.drawers.bezierPatch
|
import org.openrndr.extra.shapes.drawers.bezierPatch
|
||||||
import org.openrndr.extra.shapes.grid
|
import org.openrndr.extra.shapes.grid
|
||||||
import org.openrndr.extras.color.spaces.toOKLABa
|
import org.openrndr.extra.color.spaces.toOKLABa
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import org.openrndr.math.min
|
import org.openrndr.math.min
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ fun main() = application {
|
|||||||
drawer.stroke = ColorRGBa.BLACK
|
drawer.stroke = ColorRGBa.BLACK
|
||||||
drawer.fill = ColorRGBa.PINK
|
drawer.fill = ColorRGBa.PINK
|
||||||
drawer.contour(hobbyCurve(points, closed=true))
|
drawer.contour(hobbyCurve(points, closed=true))
|
||||||
|
drawer.fill = ColorRGBa.WHITE
|
||||||
|
drawer.circles(points, 4.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
26
orx-shapes/src/demo/kotlin/DemoHobbyCurve02.kt
Normal file
26
orx-shapes/src/demo/kotlin/DemoHobbyCurve02.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extra.shapes.AlphaShape
|
||||||
|
import org.openrndr.extra.shapes.hobbyCurve
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
program {
|
||||||
|
val points = List(20) {
|
||||||
|
Vector2(
|
||||||
|
Random.nextDouble(width*0.25, width*0.75),
|
||||||
|
Random.nextDouble(height*0.25, height*0.75)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val alphaShape = AlphaShape(points)
|
||||||
|
val c = alphaShape.create()
|
||||||
|
val hobby = hobbyCurve(c.segments.map { it.start }, closed=true)
|
||||||
|
extend {
|
||||||
|
drawer.fill = ColorRGBa.PINK
|
||||||
|
drawer.contour(hobby)
|
||||||
|
drawer.fill = ColorRGBa.WHITE
|
||||||
|
drawer.circles(points, 4.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
orx-shapes/src/jvmMain/kotlin/AlphaShape.kt
Normal file
125
orx-shapes/src/jvmMain/kotlin/AlphaShape.kt
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package org.openrndr.extra.shapes
|
||||||
|
|
||||||
|
import org.openrndr.extra.triangulation.Delaunay
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
import org.openrndr.shape.Segment
|
||||||
|
import org.openrndr.shape.ShapeContour
|
||||||
|
import org.openrndr.shape.contains
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
private fun circumradius(p1: Vector2, p2: Vector2, p3: Vector2): Double {
|
||||||
|
val a = (p2 - p1).length
|
||||||
|
val b = (p3 - p2).length
|
||||||
|
val c = (p1 - p3).length
|
||||||
|
|
||||||
|
return (a*b*c) / sqrt((a+b+c)*(b+c-a)*(c+a-b)*(a+b-c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for creating alpha shapes.
|
||||||
|
* Use the [create] method to create an alpha shape.
|
||||||
|
* @param points The points for which an alpha shape is calculated.
|
||||||
|
*/
|
||||||
|
class AlphaShape(val points: List<Vector2>) {
|
||||||
|
val delaunay = Delaunay.from(points)
|
||||||
|
|
||||||
|
private fun <A, B> Pair<A, B>.flip() = Pair(second, first)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an alpha shape.
|
||||||
|
* @param alpha The alpha parameter from the mathematical definition of an alpha shape.
|
||||||
|
* If alpha is 0.0 the alpha shape consists only of the set of input points, yielding [ShapeContour.EMPTY].
|
||||||
|
* As alpha goes to infinity, the alpha shape becomes equal to the convex hull of the input points.
|
||||||
|
* @return A closed [ShapeContour] representing the outer boundary of the alpha shape.
|
||||||
|
*/
|
||||||
|
fun create(alpha: Double): ShapeContour {
|
||||||
|
if (delaunay.points.size < 9) return ShapeContour.EMPTY
|
||||||
|
|
||||||
|
val triangles = delaunay.triangles
|
||||||
|
var allEdges = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
var perimeterEdges = mutableSetOf<Pair<Int, Int>>()
|
||||||
|
for (i in triangles.indices step 3){
|
||||||
|
val t0 = triangles[i] * 2
|
||||||
|
val t1 = triangles[i + 1] * 2
|
||||||
|
val t2 = triangles[i + 2] * 2
|
||||||
|
val p1 = getVec(t0)
|
||||||
|
val p2 = getVec(t1)
|
||||||
|
val p3 = getVec(t2)
|
||||||
|
val r = circumradius(p1, p2, p3)
|
||||||
|
if (r < alpha){
|
||||||
|
val edges = listOf(Pair(t0, t1), Pair(t1, t2), Pair(t2, t0))
|
||||||
|
for (edge in edges){
|
||||||
|
val fEdge = edge.flip()
|
||||||
|
if (edge !in allEdges && fEdge !in allEdges){
|
||||||
|
allEdges.add(edge)
|
||||||
|
perimeterEdges.add(edge)
|
||||||
|
} else {
|
||||||
|
perimeterEdges.remove(edge)
|
||||||
|
perimeterEdges.remove(fEdge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edgesToShapeContour(perimeterEdges.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the alpha shape with the smallest alpha such that all input points are contained in the alpha shape.
|
||||||
|
*/
|
||||||
|
fun create(): ShapeContour = create(determineAlpha())
|
||||||
|
|
||||||
|
private fun getVec(i: Int) = Vector2(delaunay.points[i], delaunay.points[i + 1])
|
||||||
|
|
||||||
|
private fun edgesToShapeContour(edges: List<Pair<Int, Int>>): ShapeContour {
|
||||||
|
if (edges.isEmpty()) return ShapeContour.EMPTY
|
||||||
|
val mapping = edges.toMap()
|
||||||
|
val segments = mutableListOf<Segment>()
|
||||||
|
val start = edges.first().first
|
||||||
|
var current = start
|
||||||
|
repeat(edges.size) {
|
||||||
|
val next = mapping[current]!!
|
||||||
|
segments.add(Segment(getVec(current), getVec(next)))
|
||||||
|
current = next
|
||||||
|
}
|
||||||
|
return if (current == start) {
|
||||||
|
ShapeContour(segments, closed = true)
|
||||||
|
} else {
|
||||||
|
ShapeContour.EMPTY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs binary search to find the smallest alpha such that all points are inside the alpha shape.
|
||||||
|
*/
|
||||||
|
fun determineAlpha(): Double {
|
||||||
|
// Compute bounding box to find an upper bound for the binary search
|
||||||
|
var minX = Double.POSITIVE_INFINITY
|
||||||
|
var minY = Double.POSITIVE_INFINITY
|
||||||
|
var maxX = Double.NEGATIVE_INFINITY
|
||||||
|
var maxY = Double.NEGATIVE_INFINITY
|
||||||
|
for (i in delaunay.points.indices step 2){
|
||||||
|
val x = delaunay.points[i]
|
||||||
|
val y = delaunay.points[i+1]
|
||||||
|
minX = min(minX, x)
|
||||||
|
maxX = max(maxX, x)
|
||||||
|
minY = min(minY, y)
|
||||||
|
maxY = max(maxY, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform binary search
|
||||||
|
var lower = 0.0
|
||||||
|
var upper = (maxX - minX).pow(2) + (maxY - minY).pow(2)
|
||||||
|
val precision = 0.001
|
||||||
|
|
||||||
|
while(lower < upper - precision){
|
||||||
|
val mid = (lower + upper)/2
|
||||||
|
val polygon = create(mid)
|
||||||
|
if (points.all { it in polygon }) upper = mid else lower = mid
|
||||||
|
}
|
||||||
|
|
||||||
|
return upper
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user