From acdb038c986fd2ddb3f307c2bf598a2db3690687 Mon Sep 17 00:00:00 2001 From: Ricardo Matias Date: Thu, 28 Jan 2021 12:34:44 +0100 Subject: [PATCH] Add orx-triangulation --- orx-triangulation/README.md | 69 ++ orx-triangulation/build.gradle | 21 + .../src/demo/kotlin/DemoDelaunay01.kt | 45 ++ .../src/demo/kotlin/DemoDelaunay02.kt | 42 ++ .../src/demo/kotlin/DemoVoronoi01.kt | 43 ++ orx-triangulation/src/main/kotlin/Delaunay.kt | 205 ++++++ orx-triangulation/src/main/kotlin/Voronoi.kt | 600 ++++++++++++++++++ settings.gradle | 4 +- 8 files changed, 1028 insertions(+), 1 deletion(-) create mode 100644 orx-triangulation/README.md create mode 100644 orx-triangulation/build.gradle create mode 100644 orx-triangulation/src/demo/kotlin/DemoDelaunay01.kt create mode 100644 orx-triangulation/src/demo/kotlin/DemoDelaunay02.kt create mode 100644 orx-triangulation/src/demo/kotlin/DemoVoronoi01.kt create mode 100644 orx-triangulation/src/main/kotlin/Delaunay.kt create mode 100644 orx-triangulation/src/main/kotlin/Voronoi.kt diff --git a/orx-triangulation/README.md b/orx-triangulation/README.md new file mode 100644 index 00000000..9f516e4f --- /dev/null +++ b/orx-triangulation/README.md @@ -0,0 +1,69 @@ +# orx-triangulation + +An extension for triangulating a set of points using the **Delaunay** triangulation method. From that triangulation we can also derive a **Voronoi** diagram. + +The functionality comes from a Javascript port of the following libraries: + +* [delaunator](https://github.com/ricardomatias/delaunator) (external) +* [d3-delaunay](https://github.com/d3/d3-delaunay) (the port is included in this package) + +## Usage + +### Delaunay + +The entry point is the `Delaunay` class. + +```kotlin + val points: List + val delaunay = Delaunay.from(points) + + // or + val flatPoints: DoubleArray // (x0, y0, x1, x1, x2, y2) + val delaunay = Delaunay(flatPoints) +``` + +This is how you retrieve the triangulation results: + +```kotlin +val triangles: List = delaunay.triangles() +val halfedges: List = delaunay.halfedges() +val hull: ShapeContour = delaunay.hull() + +// Updates the triangulation after the points have been modified in-place. +delaunay.update() +``` + +### Voronoi + +The bounds specifices where the Voronoi diagram will be clipped. + +```kotlin +val bounds: Rectangle + +val delaunay = Delaunay.from(points) +val voronoi = delaunay.voronoi(bounds) +// or +val voronoi = Voronoi(Delaunay.from(points), bounds) +``` + +See [To Infinity and Back Again](https://observablehq.com/@mbostock/to-infinity-and-back-again) for an interactive explanation of Voronoi cell clipping. + +This is how you retrieve th results: + +```kotlin +val cells: List = voronoi.cellsPolygons() +val cell: ShapeContour = voronoi.cellPolygon(int) // index +val circumcenters: List = voronoi.circumcenters() + +// Returns true if the cell with the specified index i contains the specified vector +val contaisVector = voronoi.contains(int, Vector2) + +// Updates the Voronoi diagram and underlying triangulation +// after the points have been modified in-place +voronoi.update() +``` + + +### Author + +Ricardo Matias / [@ricardomatias](https://github.com/ricardomatias) \ No newline at end of file diff --git a/orx-triangulation/build.gradle b/orx-triangulation/build.gradle new file mode 100644 index 00000000..dc1337d7 --- /dev/null +++ b/orx-triangulation/build.gradle @@ -0,0 +1,21 @@ +sourceSets { + demo { + java { + srcDirs = ["src/demo/kotlin"] + compileClasspath += main.getCompileClasspath() + runtimeClasspath += main.getRuntimeClasspath() + } + } +} + +dependencies { + implementation project(":orx-noise") + + implementation("com.github.ricardomatias:delaunator:1.0.0") + + demoImplementation("org.openrndr:openrndr-core:$openrndrVersion") + demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion") + demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") + demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion") + demoImplementation(sourceSets.getByName("main").output) +} \ No newline at end of file diff --git a/orx-triangulation/src/demo/kotlin/DemoDelaunay01.kt b/orx-triangulation/src/demo/kotlin/DemoDelaunay01.kt new file mode 100644 index 00000000..ba49fdae --- /dev/null +++ b/orx-triangulation/src/demo/kotlin/DemoDelaunay01.kt @@ -0,0 +1,45 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.noise.poissonDiskSampling +import org.openrndr.extra.triangulation.Delaunay +import org.openrndr.math.Vector2 +import org.openrndr.shape.Circle +import org.openrndr.shape.Rectangle + +fun main() { + application { + configure { + width = 800 + height = 800 + title = "Delaunator" + } + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val circle = Circle(Vector2(400.0), 250.0) + + val points = poissonDiskSampling(width * 1.0, height * 1.0, 30.0) + .filter { circle.contains(it) } + + val delaunay = Delaunay.from(points + circle.contour.equidistantPositions(40)) + val triangles = delaunay.triangles().map { it.contour } + + extend { + drawer.clear(ColorRGBa.BLACK) + + + for ((i, triangle) in triangles.withIndex()) { + drawer.fill = ColorRGBa.PINK.shade(1.0 - i / (triangles.size * 1.2)) + drawer.stroke = ColorRGBa.PINK.shade( i / (triangles.size * 1.0) + 0.1) + + drawer.contour(triangle) + } + } + } + } +} \ No newline at end of file diff --git a/orx-triangulation/src/demo/kotlin/DemoDelaunay02.kt b/orx-triangulation/src/demo/kotlin/DemoDelaunay02.kt new file mode 100644 index 00000000..f6daef84 --- /dev/null +++ b/orx-triangulation/src/demo/kotlin/DemoDelaunay02.kt @@ -0,0 +1,42 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.noise.poissonDiskSampling +import org.openrndr.extra.triangulation.Delaunay +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle + +fun main() { + application { + configure { + width = 800 + height = 800 + } + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val frame = Rectangle.fromCenter(Vector2(400.0), 600.0, 600.0) + + val points = poissonDiskSampling(frame.width, frame.height, 50.0).map { it + frame.corner } + + val delaunay = Delaunay.from(points) + val halfedges = delaunay.halfedges() + val hull = delaunay.hull() + + extend { + drawer.clear(ColorRGBa.BLACK) + + drawer.fill = null + drawer.stroke = ColorRGBa.PINK + drawer.contours(halfedges) + + drawer.stroke = ColorRGBa.GREEN + drawer.contour(hull) + } + } + } +} \ No newline at end of file diff --git a/orx-triangulation/src/demo/kotlin/DemoVoronoi01.kt b/orx-triangulation/src/demo/kotlin/DemoVoronoi01.kt new file mode 100644 index 00000000..e53a5a9e --- /dev/null +++ b/orx-triangulation/src/demo/kotlin/DemoVoronoi01.kt @@ -0,0 +1,43 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.noise.poissonDiskSampling +import org.openrndr.extra.triangulation.Delaunay +import org.openrndr.math.Vector2 +import org.openrndr.shape.Circle +import org.openrndr.shape.Rectangle + +fun main() { + application { + configure { + width = 800 + height = 800 + } + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val circle = Circle(Vector2(400.0), 250.0) + val frame = Rectangle.fromCenter(Vector2(400.0), 600.0, 600.0) + + val points = poissonDiskSampling(width * 1.0, height * 1.0, 30.0) + .filter { circle.contains(it) } + + val delaunay = Delaunay.from(points + circle.contour.equidistantPositions(40)) + val voronoi = delaunay.voronoi(frame) + + val cells = voronoi.cellsPolygons() + + extend { + drawer.clear(ColorRGBa.BLACK) + + drawer.fill = null + drawer.stroke = ColorRGBa.PINK + drawer.contours(cells) + } + } + } +} \ No newline at end of file diff --git a/orx-triangulation/src/main/kotlin/Delaunay.kt b/orx-triangulation/src/main/kotlin/Delaunay.kt new file mode 100644 index 00000000..7c3ea3d4 --- /dev/null +++ b/orx-triangulation/src/main/kotlin/Delaunay.kt @@ -0,0 +1,205 @@ +package org.openrndr.extra.triangulation + +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import org.openrndr.shape.Triangle +import org.openrndr.shape.contour +import org.openrndr.shape.contours +import com.github.ricardomatias.Delaunator +import kotlin.math.pow + +/* +ISC License + +Copyright 2021 Ricardo Matias. + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + +/** + * Use [from] static method to use the delaunay triangulation + * + * @description Port of d3-delaunay (JavaScript) library - https://github.com/d3/d3-delaunay + * @property points flat positions' array - [x0, y0, x1, y1..] + * + * @since 9258fa3 - commit + * @author Ricardo Matias + */ +@Suppress("unused") +class Delaunay(val points: DoubleArray) { + companion object { + /** + * Entry point for the delaunay triangulation + * + * @property points a list of 2D points + */ + fun from(points: List): Delaunay { + val n = points.size + val coords = DoubleArray(n * 2) + + for (i in points.indices) { + val p = points[i] + coords[2 * i] = p.x + coords[2 * i + 1] = p.y + } + + return Delaunay(coords) + } + } + + private var delaunator = Delaunator(points) + + val inedges = IntArray(points.size / 2) { -1 } + private val hullIndex = IntArray(points.size / 2) { -1 } + + private var collinear = IntArray(points.size / 2) { it } + + var halfedges = delaunator.halfedges + var hull = delaunator.hull + var triangles = delaunator.triangles + + init { + init() + } + + fun update() { + delaunator.update() + init() + } + + fun init() { + halfedges = delaunator.halfedges + hull = delaunator.hull + triangles = delaunator.triangles + + // Compute an index from each point to an (arbitrary) incoming halfedge + // Used to give the first neighbor of each point for this reason, + // on the hull we give priority to exterior halfedges + for (e in halfedges.indices) { + val p = triangles[nextHalfedge(e)] + + if (halfedges[e] == -1 || inedges[p] == -1) inedges[p] = e + } + + for (i in hull.indices) { + hullIndex[hull[i]] = i + } + + // degenerate case: 1 or 2 (distinct) points + if (hull.size in 1..2) { + triangles = IntArray(3) { -1 } + halfedges = IntArray(3) { -1 } + triangles[0] = hull[0] + triangles[1] = hull[1] + triangles[2] = hull[1] + inedges[hull[0]] = 1 + if (hull.size == 2) inedges[hull[1]] = 0 + } + } + + fun triangles(): List { + val list = mutableListOf() + + 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 = Vector2(points[t0], points[t0 + 1]) + val p2 = Vector2(points[t1], points[t1 + 1]) + val p3 = Vector2(points[t2], points[t2 + 1]) + + // originally they are defined *counterclockwise* + list.add(Triangle(p3, p2, p1)) + } + + return list + } + + // Inner edges of the delaunay triangulation (without hull) + fun halfedges() = contours { + for (i in halfedges.indices) { + val j = halfedges[i] + + if (j < i) continue + val ti = triangles[i] * 2 + val tj = triangles[j] * 2 + + moveTo(points[ti], points[ti + 1]) + lineTo(points[tj], points[tj + 1]) + } + } + + fun hull() = contour { + for (h in hull) { + moveOrLineTo(points[2 * h], points[2 * h + 1]) + } + close() + } + + fun find(x: Double, y: Double, i: Int = 0): Int { + val x0 = +x + val y0 = +y + var i0 = i + + if ((x0 != x) || (y0 != y)) return -1 + + val i1 = i0 + var c = step(i0, x, y) + + while (c >= 0 && c != i && c != i1) { + i0 = c + c = step(i0, x, y) + } + return c + } + + fun nextHalfedge(e: Int) = if (e % 3 == 2) e - 2 else e + 1 + fun prevHalfedge(e: Int) = if (e % 3 == 0) e + 2 else e - 1 + + fun step(i: Int, x: Double, y: Double): Int { + if (inedges[i] == -1 || points.isEmpty()) return (i + 1) % (points.size shr 1) + + var c = i + var dc = (x - points[i * 2]).pow(2) + (y - points[i * 2 + 1]).pow(2) + val e0 = inedges[i] + var e = e0 + do { + val t = triangles[e] + val dt = (x - points[t * 2]).pow(2) + (y - points[t * 2 + 1]).pow(2) + + if (dt < dc) { + dc = dt + c = t + } + + e = nextHalfedge(e) + + if (triangles[e] != i) break // bad triangulation + + e = halfedges[e] + + if (e == -1) { + e = hull[(hullIndex[i] + 1) % hull.size] + if (e != t) { + if ((x - points[e * 2]).pow(2) + (y - points[e * 2 + 1]).pow(2) < dc) return e + } + break + } + } while (e != e0) + + return c + } + + fun voronoi(bounds: Rectangle): Voronoi = Voronoi(this, bounds) +} \ No newline at end of file diff --git a/orx-triangulation/src/main/kotlin/Voronoi.kt b/orx-triangulation/src/main/kotlin/Voronoi.kt new file mode 100644 index 00000000..4c456908 --- /dev/null +++ b/orx-triangulation/src/main/kotlin/Voronoi.kt @@ -0,0 +1,600 @@ +package org.openrndr.extra.triangulation + +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import org.openrndr.shape.ShapeContour +import kotlin.math.abs + +/* +ISC License + +Copyright 2021 Ricardo Matias. + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +*/ + + +/** + * This is a fast library for computing the Voronoi diagram of a set of two-dimensional points. + * The Voronoi diagram is constructed by connecting the circumcenters of adjacent triangles + * in the Delaunay triangulation. + * + * @description Port of d3-delaunay (JavaScript) library - https://github.com/d3/d3-delaunay + * @property points flat positions' array - [x0, y0, x1, y1..] + * + * @since 9258fa3 - commit + * @author Ricardo Matias + */ +class Voronoi(val delaunay: Delaunay, val bounds: Rectangle) { + private val _circumcenters = DoubleArray(delaunay.points.size * 2) + val circumcenters = _circumcenters.copyOf(delaunay.triangles.size / 3 * 2) + + val vectors = DoubleArray(delaunay.points.size * 2) + + init { + init() + } + + fun update() { + delaunay.update() + init() + } + + fun init() { + val points = delaunay.points + val triangles = delaunay.triangles + val hull = delaunay.hull + + // Compute circumcenters + var i = 0 + var j = 0 + + var x: Double + var y: Double + + while (i < triangles.size) { + val t1 = triangles[i] * 2 + val t2 = triangles[i + 1] * 2 + val t3 = triangles[i + 2] * 2 + val x1 = points[t1] + val y1 = points[t1 + 1] + val x2 = points[t2] + val y2 = points[t2 + 1] + val x3 = points[t3] + val y3 = points[t3 + 1] + + val dx = x2 - x1 + val dy = y2 - y1 + val ex = x3 - x1 + val ey = y3 - y1 + val bl = dx * dx + dy * dy + val cl = ex * ex + ey * ey + val ab = (dx * ey - dy * ex) * 2 + + when { + ab == 0.0 -> { + // degenerate case (collinear diagram) + x = (x1 + x3) / 2 - 1e8 * ey + y = (y1 + y3) / 2 + 1e8 * ex + } + abs(ab) < 1e-8 -> { + // almost equal points (degenerate triangle) + x = (x1 + x3) / 2 + y = (y1 + y3) / 2 + } + else -> { + val d = 1 / ab + x = x1 + (ey * bl - dy * cl) * d + y = y1 + (dx * cl - ex * bl) * d + } + } + + circumcenters[j] = x + circumcenters[j + 1] = y + + i += 3 + j += 2 + } + + // Compute exterior cell rays. + var h = hull[hull.size - 1] + var p0: Int + var p1 = h * 4 + var x0: Double + var x1 = points[2 * h] + var y0: Double + var y1 = points[2 * h + 1] + var y01: Double + var x10: Double + + vectors.fill(0.0) + + for (idx in hull.indices) { + h = hull[idx] + p0 = p1 + x0 = x1 + y0 = y1 + p1 = h * 4 + x1 = points[2 * h] + y1 = points[2 * h + 1] + + y01 = y0 - y1 + x10 = x1 - x0 + + vectors[p0 + 2] = y01 + vectors[p1] = y01 + vectors[p0 + 3] = x10 + vectors[p1 + 1] = x10 + } + + } + + fun cellsPolygons(): List { + val points = delaunay.points + val cells = mutableListOf() + + for (i in 0 until (points.size / 2)) { + cellPolygon(i)?.let { + cells.add(it) + } + } + + return cells + } + + fun cellPolygon(i: Int): ShapeContour? { + val points = clip(i) + + if (points == null || points.isEmpty()) return null + + val polygon = mutableListOf(Vector2(points[0], points[1])) + var n = points.size + + while (points[0] == points[n-2] && points[1] == points[n-1] && n > 1) n -= 2 + + for (idx in 2 until n step 2) { + if (points[idx] != points[idx - 2] || points[idx + 1] != points[idx - 1]) { + polygon.add(Vector2(points[idx], points[idx + 1])) + } + } + + return ShapeContour.fromPoints(polygon, true) + } + + fun circumcenters() = circumcenters.toList().windowed(2, 2).map { + Vector2(it[0], it[1]) + } + + fun contains(i: Int, v: Vector2): Boolean { + return contains(i, v.x, v.y) + } + + private fun cell(i: Int): MutableList? { + val inedges = delaunay.inedges + val halfedges = delaunay.halfedges + val triangles = delaunay.triangles + + val e0 = inedges[i] + + if (e0 == -1) return null // coincident point + + val points = mutableListOf() + + var e = e0 + + do { + val t = Math.floorDiv(e, 3) // triangle of edge + + points.add(circumcenters[t * 2]) + points.add(circumcenters[t * 2 + 1]) + + e = if (e % 3 == 2) e - 2 else e + 1 // next half edge + + if (triangles[e] != i) break + + e = halfedges[e] + } while (e != e0 && e != -1) + + return points + } + + private fun clip(i: Int): List? { + // degenerate case (1 valid point: return the box) + if (i == 0 && delaunay.hull.size == 1) { + return listOf(bounds.xmax, bounds.ymin, bounds.xmax, bounds.ymax, bounds.xmin, bounds.ymax, bounds.xmin, bounds.ymin) + } + + val points = cell(i) ?: return null + + val clipVectors = vectors + val v = i * 4 + + val a = !clipVectors[v].isFalsy() + val b = !clipVectors[v + 1].isFalsy() + + return if (a || b) { + this.clipInfinite(i, points, clipVectors[v], clipVectors[v +1], clipVectors[v + 2], clipVectors[v + 3]) + } else { + this.clipFinite(i, points) + } + } + + private fun clipInfinite( + i: Int, + points: MutableList, + vx0: Double, + vy0: Double, + vxn: Double, + vyn: Double + ): List? { + var P: MutableList? = points.mutableCopyOf().also { list -> + // SHAKY + this.project(list[0], list[1], vx0, vy0)?.also { + list.addAll(0, listOf(it.x, it.y)) + } + + this.project(list[list.size - 2], list[list.size - 1], vxn, vyn)?.also { + list.addAll(0, listOf(it.x, it.y)) + } + } + + P = clipFinite(i, P!!) + + if (P != null) { + var n = P.size + var c0: Int? + var c1 = edgeCode(P[n - 2], P[n - 1]) + var j = 0 + + while (j < n) { + c0 = c1 + c1 = edgeCode(P[j], P[j + 1]) + + if ((c0 and c1) != 0) { + j = edge(i, c0, c1, P, j) + n = P.size + } + + j += 2 + } + } else if (contains(i, (bounds.xmin + bounds.xmax) / 2, (bounds.ymin + bounds.ymax) / 2)) { + P = mutableListOf(bounds.xmin, bounds.ymin, bounds.xmax, bounds.ymin, bounds.xmax, bounds.ymax, bounds.xmin, bounds.ymax) + } + + return P + } + + private fun clipFinite(i: Int, points: MutableList): MutableList? { + val n = points.size + + val P = mutableListOf() + var x0: Double + var y0: Double + var x1= points[n - 2] + var y1= points[n - 1] + var c0: Int + var c1: Int = regionCode(x1, y1) + var e0: Int? = null + var e1: Int? = null + + for (j in 0 until n step 2) { + x0 = x1 + y0 = y1 + x1 = points[j] + y1 = points[j + 1] + c0 = c1 + c1 = regionCode(x1, y1) + + if (c0 == 0 && c1 == 0) { + e0 = e1 + e1 = 0 + + P.add(x1) + P.add(y1) + } else { + var S: DoubleArray? + var sx0: Double + var sy0: Double + var sx1: Double + var sy1: Double + + if (c0 == 0) { + S = clipSegment(x0, y0, x1, y1, c0, c1) + if (S == null) continue +// sx0 = S[0] +// sy0 = S[1] + sx1 = S[2] + sy1 = S[3] + } else { + S = clipSegment(x1, y1, x0, y0, c1, c0) + if (S == null) continue + sx1 = S[0] + sy1 = S[1] + sx0 = S[2] + sy0 = S[3] + + e0 = e1 + e1 = this.edgeCode(sx0, sy0) + + if (e0.isTruthy() && e1.isTruthy()) this.edge(i, e0!!, e1, P, P.size) + + P.add(sx0) + P.add(sy0) + } + + e0 = e1 + e1 = this.edgeCode(sx1, sy1); + + if (e0.isTruthy() && e1.isTruthy()) this.edge(i, e0!!, e1, P, P.size); + + P.add(sx1) + P.add(sy1) + } + } + + if (P.isNotEmpty()) { + e0 = e1 + e1 = this.edgeCode(P[0], P[1]) + + if (e0.isTruthy() && e1.isTruthy()) this.edge(i, e0!!, e1!!, P, P.size); + } else if (this.contains(i, (bounds.xmin + bounds.xmax) / 2, (bounds.ymin + bounds.ymax) / 2)) { + return mutableListOf(bounds.xmax, bounds.ymin, bounds.xmax, bounds.ymax, bounds.xmin, bounds.ymax, bounds.xmin, bounds.ymin) + } else { + return null + } + + return P + } + + private fun clipSegment(x0: Double, y0: Double, x1: Double, y1: Double, c0: Int, c1: Int): DoubleArray? { + var nx0: Double = x0 + var ny0: Double = y0 + var nx1: Double = x1 + var ny1: Double = y1 + var nc0: Int = c0 + var nc1: Int = c1 + + while(true) { + if (nc0 == 0 && nc1 == 0) return doubleArrayOf(nx0, ny0, nx1, ny1) + // SHAKY STUFF + if ((nc0 and nc1) != 0) return null + + var x: Double + var y: Double + val c: Int = if (nc0 != 0) nc0 else nc1 + + when { + (c and 0b1000) != 0 -> { + x = nx0 + (nx1 - nx0) * (bounds.ymax - ny0) / (ny1 - ny0) + y = bounds.ymax; + } + (c and 0b0100) != 0 -> { + x = nx0 + (nx1 - nx0) * (bounds.ymin - ny0) / (ny1 - ny0) + y = bounds.ymin + } + (c and 0b0010) != 0 -> { + y = ny0 + (ny1 - ny0) * (bounds.xmax - nx0) / (nx1 - nx0) + x = bounds.xmax + } + else -> { + y = ny0 + (ny1 - ny0) * (bounds.xmin - nx0) / (nx1 - nx0) + x = bounds.xmin; + } + } + + if (nc0 != 0) { + nx0 = x + ny0 = y + nc0 = this.regionCode(nx0, ny0) + } else { + nx1 = x + ny1 = y + nc1 = this.regionCode(nx1, ny1) + } + } + } + + private fun regionCode(x: Double, y: Double): Int { + val code = when { + x < bounds.xmin -> 0b0001 + x > bounds.xmax -> 0b0010 + else -> 0b0000 + } + return code or when { + y < bounds.ymin -> 0b0100 + y > bounds.ymax -> 0b1000 + else -> 0b0000 + } + } + + + private fun contains(i: Int, x: Double, y: Double): Boolean { +// if ((x = +x, x !== x) || (y = +y, y !== y)) return false; + return this.delaunay.step(i, x, y) == i; + } + + private fun edge(i: Int, e0: Int, e1: Int, p: MutableList, j: Int): Int { + var j = j + var e = e0 + while(e != e1) { + var x: Double = Double.NaN + var y: Double = Double.NaN + + when(e) { + 0b0101 -> { // top-left + e = 0b0100 + continue + } + 0b0100 -> { // top + e = 0b0110 + x = bounds.xmax + y = bounds.ymin + break + } + 0b0110 -> { // top-right + e = 0b0010 + continue + } + 0b0010 -> { // right + e = 0b1010 + x = bounds.xmax + y = bounds.ymax + break + } + 0b1010 -> { // bottom-right + e = 0b1000 + continue + } + 0b1000 -> { // bottom + e = 0b0001 + x = bounds.xmin + y = bounds.ymax + break + } + 0b1001 -> { // bottom-left + e = 0b0001 + continue + } + 0b0001 -> { // left + e = 0b0101 + x = bounds.xmin + y = bounds.ymin + break + } + } + + if ((p[j] != x || p[j + 1] != y) && contains(i, x, y)) { + p.add(j, y) + p.add(j, x) + j += 2 + } + } + + if (p.size > 4) { + var idx = 0 + + while (idx < p.size) { + val j = (idx + 2) % p.size + val k = (idx + 4) % p.size + + if (p[idx] == p[j] && p[j] == p[k] + || p[idx + 1] == p[j + 1] && p[j + 1] == p[k + 1]) { + // SHAKY + p.removeAt(j) + p.removeAt(j) + idx -= 2 + } + + idx += 2 + } + } + return j + } + + private fun project(x0: Double, y0: Double, vx: Double, vy: Double): Vector2? { + var t = Double.POSITIVE_INFINITY + var c: Double + var x = Double.NaN + var y = Double.NaN + + // top + if(vy < 0) { + if (y0 <= bounds.ymin) return null + c = (bounds.ymin - y0) / vy + + if(c < t) { + t = c + + y = bounds.ymin + x = x0 + c * vx + } + } + // bottom + else if (vy > 0) { + if (y0 >= bounds.ymax) return null + c = (bounds.ymax - y0) / vy + + if( c < t) { + t = c + + y = bounds.ymax + x = x0 + c * vx + } + } + // right + if (vx > 0) { + if (x0 >= bounds.xmax) return null + c = (bounds.xmax - x0) / vx + + if (c < t) { + t = c + + x = bounds.xmax + y = y0 + t * vy + } + // left + } else if (vx < 0) { + if (x0 <= bounds.xmin) return null + c = (bounds.xmin - x0) / vx + + if (c < t) { + t = c + + x = bounds.xmin + y = y0 + t * vy + } + } + + if(x.isNaN() || y.isNaN()) return null + + return Vector2(x, y) + } + + private fun edgeCode(x: Double, y: Double): Int { + val code = when (x) { + bounds.xmin -> 0b0001 + bounds.xmax -> 0b0010 + else -> 0b0000 + } + + return code or when (y) { + bounds.ymin -> 0b0100 + bounds.ymax -> 0b1000 + else -> 0b0000 + } + } + +} + +private fun Int?.isTruthy(): Boolean { + return (this != null && this != 0) +} + +private fun List.mutableCopyOf(): MutableList { + val original = this + return mutableListOf().apply { addAll(original) } +} + +private val Rectangle.xmin: Double + get() = this.corner.x + +private val Rectangle.xmax: Double + get() = this.corner.x + width + +private val Rectangle.ymin: Double + get() = this.corner.y + +private val Rectangle.ymax: Double + get() = this.corner.y + height + +private fun Double?.isFalsy() = this == null || this == -0.0 || this == 0.0 || isNaN() \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 650174b8..9f727eb5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -54,6 +54,7 @@ include 'openrndr-demos', 'orx-tensorflow-natives-windows', 'orx-timer', 'orx-time-operators', + 'orx-triangulation', 'orx-kinect-common', 'orx-kinect-v1', 'orx-kinect-v1-natives-linux-arm64', @@ -61,4 +62,5 @@ include 'openrndr-demos', 'orx-kinect-v1-natives-macos', 'orx-kinect-v1-natives-windows', 'orx-kinect-v1-demo', - 'orx-video-profiles' \ No newline at end of file + 'orx-video-profiles' +