FloatEarcut.kt
src/jvmMain/kotlin/afield/geometry/path/FloatEarcut.kt · 800 lines
package afield.geometry.path
import afield.graphics.three.three
import afield.lang.remotejs.JsFun
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
object FloatEarcut
object FloatEarcut {
fun earcut(data: FloatArray, holeIndices: IntArray? = null, dim: Int = 2): MutableList<Int>
Triangulates the given polygon
@param data is a flat array of vertice coordinates like [x0,y0, x1,y1, x2,y2, ...]. @param holeIndices is an array of hole indices if any (e.g. [5, 8] for a 12-vertice input would mean one hole with vertices 5-7 and another with 8-11). @param dim is the number of coordinates per vertice in the input array @return List containing groups of three vertice indices in the resulting array forms a triangle.
body — 223 lines, from line 19
@JvmOverloads
fun earcut(data: FloatArray, holeIndices: IntArray? = null, dim: Int = 2): MutableList<Int> {
val hasHoles = holeIndices != null && holeIndices.size > 0
val outerLen = if (hasHoles) holeIndices[0] * dim else data.size
var outerNode = linkedList(data, 0, outerLen, dim, true)
val triangles: MutableList<Int> = ArrayList<Int>()
if (outerNode.next === outerNode.prev) return triangles
var minX = 0f
var minY = 0f
var maxX = 0f
var maxY = 0f
var invSize = Float.Companion.MIN_VALUE
if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim)
// if the shape is not too simple, we'll use z-order curve hash later;
// calculate polygon bbox
if (data.size > 80 * dim) {
maxX = data[0]
minX = maxX
maxY = data[1]
minY = maxY
var i = dim
while (i < outerLen) {
val x = data[i]
val y = data[i + 1]
if (x < minX) minX = x
if (y < minY) minY = y
if (x > maxX) maxX = x
if (y > maxY) maxY = y
i += dim
}
// minX, minY and size are later used to transform coords into
// integers for z-order calculation
invSize = max(maxX - minX, maxY - minY)
invSize = if (invSize != 0.0f) 1.0f / invSize else 0.0f
}
earcutLinked(outerNode, triangles, dim, minX, minY, invSize, Int.Companion.MIN_VALUE)
return triangles
}
private fun earcutLinked(
ear: Node?,
triangles: MutableList<Int>,
dim: Int,
minX: Float,
minY: Float,
invSize: Float,
pass: Int
) {
var ear = ear
if (ear == null) return
// interlink polygon nodes in z-order
if (pass == Int.Companion.MIN_VALUE && invSize != Float.Companion.MIN_VALUE) indexCurve(
ear,
minX,
minY,
invSize
)
var stop: Node? = ear
// iterate through ears, slicing them one by one
while (ear!!.prev !== ear.next) {
val prev = ear.prev
val next = ear.next
if (if (invSize != Float.Companion.MIN_VALUE) FloatEarcut.isEarHashed(
ear,
minX,
minY,
invSize
) else FloatEarcut.isEar(ear)
) {
// cut off the triangle
triangles.add(prev!!.i / dim)
triangles.add(ear.i / dim)
triangles.add(next!!.i / dim)
FloatEarcut.removeNode(ear)
// skipping the next vertice leads to less sliver triangles
ear = next.next
stop = next.next
continue
}
ear = next
// if we looped through the whole remaining polygon and can't find
// any more ears
if (ear === stop) {
// try filtering points and slicing again
if (pass == Int.Companion.MIN_VALUE) {
earcutLinked(filterPoints(ear, null), triangles, dim, minX, minY, invSize, 1)
// if this didn't work, try curing all small
// self-intersections locally
} else if (pass == 1) {
ear = cureLocalIntersections(filterPoints(ear, null)!!, triangles, dim)
earcutLinked(ear, triangles, dim, minX, minY, invSize, 2)
// as a last resort, try splitting the remaining polygon
// into two
} else if (pass == 2) {
splitEarcut(ear!!, triangles, dim, minX, minY, invSize)
}
break
}
}
}
private fun splitEarcut(
start: Node,
triangles: MutableList<Int>,
dim: Int,
minX: Float,
minY: Float,
size: Float
) {
// look for a valid diagonal that divides the polygon into two
var a: Node? = start
do {
var b = a!!.next!!.next
while (b !== a!!.prev) {
if (a.i != b!!.i && FloatEarcut.isValidDiagonal(a, b)) {
// split the polygon in two by the diagonal
var c = FloatEarcut.splitPolygon(a, b)
// filter colinear points around the cuts
a = filterPoints(a, a.next)!!
c = filterPoints(c, c.next)!!
// run earcut on each half
earcutLinked(a, triangles, dim, minX, minY, size, Int.Companion.MIN_VALUE)
earcutLinked(c, triangles, dim, minX, minY, size, Int.Companion.MIN_VALUE)
return
}
b = b.next
}
a = a.next
} while (a !== start)
}
private fun isValidDiagonal(a: Node, b: Node): Boolean {
//return a.next.i != b.i && a.prev.i != b.i && !intersectsPolygon(a, b) && locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b);
return a.next!!.i != b.i && a.prev!!.i != b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
(locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(FloatEarcut.area(a.prev!!, a, b.prev!!) != 0f || FloatEarcut.area(
a,
b.prev!!,
b
) != 0f) || // does not create opposite-facing sectors
equals(a, b) && FloatEarcut.area(a.prev!!, a, a.next!!) > 0 && FloatEarcut.area(
b.prev!!,
b,
b.next!!
) > 0) // special zero-length case
}
private fun middleInside(a: Node, b: Node): Boolean {
var p: Node? = a
var inside = false
val px = (a.x + b.x) / 2
val py = (a.y + b.y) / 2
do {
if (((p!!.y > py) != (p.next!!.y > py)) && (px < (p.next!!.x - p.x) * (py - p.y) / (p.next!!.y - p.y) + p.x)) inside =
!inside
p = p.next
} while (p !== a)
return inside
}
private fun intersectsPolygon(a: Node, b: Node): Boolean {
var p: Node? = a
do {
if (p!!.i != a.i && p.next!!.i != a.i && p.i != b.i && p.next!!.i != b.i && FloatEarcut.intersects(
p,
p.next!!,
a,
b
)
) return true
p = p.next
} while (p !== a)
return false
}
private fun intersects(p1: Node, q1: Node, p2: Node, q2: Node): Boolean {
if ((equals(p1, p2) && equals(q1, q2)) || (equals(p1, q2) && equals(p2, q1))) return true
val o1 = sign(area(p1, q1, p2))
val o2 = sign(area(p1, q1, q2))
val o3 = sign(area(p2, q2, p1))
val o4 = sign(area(p2, q2, q1))
if (o1 != o2 && o3 != o4) return true // general case
if (o1 == 0f && onSegment(p1, p2, q1)) return true // p1, q1 and p2 are collinear and p2 lies on p1q1
if (o2 == 0f && onSegment(p1, q2, q1)) return true // p1, q1 and q2 are collinear and q2 lies on p1q1
if (o3 == 0f && onSegment(p2, p1, q2)) return true // p2, q2 and p1 are collinear and p1 lies on p2q2
if (o4 == 0f && onSegment(p2, q1, q2)) return true // p2, q2 and q1 are collinear and q1 lies on p2q2
return false
}
for collinear points p, q, r, check if point q lies on segment pr
source — 147 lines, from line 244
private fun onSegment(p: Node, q: Node, r: Node): Boolean {
return q.x <= max(p.x, r.x) && q.x >= min(p.x, r.x) && q.y <= max(p.y, r.y) && q.y >= min(p.y, r.y)
}
private fun sign(num: Float): Float {
return (if (num > 0) 1 else if (num < 0) -1 else 0).toFloat()
}
private fun cureLocalIntersections(start: Node, triangles: MutableList<Int>, dim: Int): Node {
var start = start
var p: Node? = start
do {
val a = p!!.prev
val b = p.next!!.next
if (!FloatEarcut.equals(a!!, b!!) && FloatEarcut.intersects(
a,
p,
p.next!!,
b
) && FloatEarcut.locallyInside(a, b) && FloatEarcut.locallyInside(b, a)
) {
triangles.add(a.i / dim)
triangles.add(p.i / dim)
triangles.add(b.i / dim)
// remove two nodes involved
FloatEarcut.removeNode(p)
FloatEarcut.removeNode(p.next!!)
start = b
p = start
}
p = p.next
} while (p !== start)
return filterPoints(p, null)!!
}
private fun isEar(ear: Node): Boolean {
val a = ear.prev
val b: Node? = ear
val c = ear.next
if (FloatEarcut.area(a!!, b!!, c!!) >= 0) return false // reflex, can't be an ear
// now make sure we don't have other points inside the potential ear
var p = ear.next!!.next
while (p !== ear.prev) {
if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p!!.x, p.y) && FloatEarcut.area(
p.prev!!,
p,
p.next!!
) >= 0
) return false
p = p.next
}
return true
}
private fun isEarHashed(ear: Node, minX: Float, minY: Float, invSize: Float): Boolean {
val a = ear.prev
val b = ear
val c = ear.next
if (FloatEarcut.area(a!!, b, c!!) >= 0) return false // reflex, can't be an ear
// triangle bbox; min & max are calculated like this for speed
val minTX = if (a.x < b.x) (if (a.x < c.x) a.x else c.x) else (if (b.x < c.x) b.x else c.x)
val minTY = if (a.y < b.y) (if (a.y < c.y) a.y else c.y) else (if (b.y < c.y) b.y else c.y)
val maxTX = if (a.x > b.x) (if (a.x > c.x) a.x else c.x) else (if (b.x > c.x) b.x else c.x)
val maxTY = if (a.y > b.y) (if (a.y > c.y) a.y else c.y) else (if (b.y > c.y) b.y else c.y)
// z-order range for the current triangle bbox;
val minZ = zOrder(minTX, minTY, minX, minY, invSize)
val maxZ = zOrder(maxTX, maxTY, minX, minY, invSize)
// first look for points inside the triangle in increasing z-order
var p = ear.prevZ
var n = ear.nextZ
while (p != null && p.z >= minZ && n != null && n.z <= maxZ) {
if (p !== ear.prev && p !== ear.next && pointInTriangle(
a.x,
a.y,
b.x,
b.y,
c.x,
c.y,
p.x,
p.y
) && FloatEarcut.area(p.prev!!, p, p.next!!) >= 0
) return false
p = p.prevZ
if (n !== ear.prev && n !== ear.next && pointInTriangle(
a.x,
a.y,
b.x,
b.y,
c.x,
c.y,
n.x,
n.y
) && FloatEarcut.area(n.prev!!, n, n.next!!) >= 0
) return false
n = n.nextZ
}
// look for remaining points in decreasing z-order
while (p != null && p.z >= minZ) {
if (p !== ear.prev && p !== ear.next && pointInTriangle(
a.x,
a.y,
b.x,
b.y,
c.x,
c.y,
p.x,
p.y
) && FloatEarcut.area(p.prev!!, p, p.next!!) >= 0
) return false
p = p.prevZ
}
// look for remaining points in increasing z-order
while (n != null && n.z <= maxZ) {
if (n !== ear.prev && n !== ear.next && pointInTriangle(
a.x,
a.y,
b.x,
b.y,
c.x,
c.y,
n.x,
n.y
) && FloatEarcut.area(n.prev!!, n, n.next!!) >= 0
) return false
n = n.nextZ
}
return true
}
z-order of a point given coords and inverse of the longer side of data bbox
source — 193 lines, from line 393
private fun zOrder(x: Float, y: Float, minX: Float, minY: Float, invSize: Float): Float {
// coords are transformed into non-negative 15-bit integer range
var lx: Int = (32767 * (x - minX) * invSize).toInt()
var ly: Int = (32767 * (y - minY) * invSize).toInt()
lx = (lx or (lx shl 8)) and 0x00FF00FF
lx = (lx or (lx shl 4)) and 0x0F0F0F0F
lx = (lx or (lx shl 2)) and 0x33333333
lx = (lx or (lx shl 1)) and 0x55555555
ly = (ly or (ly shl 8)) and 0x00FF00FF
ly = (ly or (ly shl 4)) and 0x0F0F0F0F
ly = (ly or (ly shl 2)) and 0x33333333
ly = (ly or (ly shl 1)) and 0x55555555
return (lx or (ly shl 1)).toFloat()
}
private fun indexCurve(start: Node, minX: Float, minY: Float, invSize: Float) {
var p: Node? = start
do {
if (p!!.z == Float.Companion.MIN_VALUE) p.z = zOrder(p.x, p.y, minX, minY, invSize)
p.prevZ = p.prev
p.nextZ = p.next
p = p.next
} while (p !== start)
p.prevZ!!.nextZ = null
p.prevZ = null
sortLinked(p)
}
private fun sortLinked(list: Node?): Node? {
var list = list
var inSize = 1
var numMerges: Int
do {
var p = list
list = null
var tail: Node? = null
numMerges = 0
while (p != null) {
numMerges++
var q: Node? = p
var pSize = 0
for (i in 0..<inSize) {
pSize++
q = q!!.nextZ
if (q == null) break
}
var qSize = inSize
while (pSize > 0 || (qSize > 0 && q != null)) {
val e: Node?
if (pSize == 0) {
e = q
q = q!!.nextZ
qSize--
} else if (qSize == 0 || q == null) {
e = p
p = p!!.nextZ
pSize--
} else if (p!!.z <= q.z) {
e = p
p = p.nextZ
pSize--
} else {
e = q
q = q.nextZ
qSize--
}
if (tail != null) tail.nextZ = e
else list = e
e.prevZ = tail
tail = e
}
p = q
}
tail!!.nextZ = null
inSize *= 2
} while (numMerges > 1)
return list
}
private fun eliminateHoles(data: FloatArray, holeIndices: IntArray, outerNode: Node, dim: Int): Node {
var outerNode: Node? = outerNode
val queue: MutableList<Node> = ArrayList<Node>()
val len = holeIndices.size
for (i in 0..<len) {
val start = holeIndices[i] * dim
val end = if (i < len - 1) holeIndices[i + 1] * dim else data.size
val list = linkedList(data, start, end, dim, false)
if (list === list.next) list.steiner = true
queue.add(getLeftmost(list))
}
queue.sortWith (object : Comparator<Node> {
override fun compare(o1: Node, o2: Node): Int {
if (o1.x - o2.x > 0) return 1
else if (o1.x - o2.x < 0) return -2
return 0
}
})
for (node in queue) {
eliminateHole(node, outerNode)
outerNode = filterPoints(outerNode, outerNode?.next)
}
return outerNode!!
}
private fun filterPoints(start: Node?, end: Node?): Node? {
var end = end
if (start == null) return start
if (end == null) end = start
var p: Node? = start
var again: Boolean
do {
again = false
if (!p!!.steiner && FloatEarcut.equals(p, p.next!!) || FloatEarcut.area(
p.prev!!,
p,
p.next!!
) == 0f
) {
FloatEarcut.removeNode(p)
end = p.prev
p = end
if (p === p!!.next) break
again = true
} else {
p = p.next
}
} while (again || p !== end)
return end!!
}
private fun equals(p1: Node, p2: Node): Boolean {
return p1.x == p2.x && p1.y == p2.y
}
private fun area(p: Node, q: Node, r: Node): Float {
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
}
private fun eliminateHole(hole: Node, outerNode: Node?) {
var outerNode = outerNode
outerNode = FloatEarcut.findHoleBridge(hole, outerNode!!)
if (outerNode != null) {
val b = splitPolygon(outerNode, hole)
// filter collinear points around the cuts
filterPoints(outerNode, outerNode.next)
filterPoints(b, b.next)
}
}
private fun splitPolygon(a: Node, b: Node): Node {
val a2 = Node(a.i, a.x, a.y)
val b2 = Node(b.i, b.x, b.y)
val an = a.next
val bp = b.prev
a.next = b
b.prev = a
a2.next = an
an!!.prev = a2
b2.next = a2
a2.prev = b2
bp!!.next = b2
b2.prev = bp
return b2
}
David Eberly's algorithm for finding a bridge between hole and outer polygon
source — 87 lines, from line 589
private fun findHoleBridge(hole: Node, outerNode: Node): Node? {
var p: Node? = outerNode
val hx = hole.x
val hy = hole.y
var qx = -Float.Companion.MAX_VALUE
var m: Node? = null
// find a segment intersected by a ray from the hole's leftmost point to
// the left;
// segment's endpoint with lesser x will be potential connection point
do {
if (hy <= p!!.y && hy >= p.next!!.y) {
val x = p.x + (hy - p.y) * (p.next!!.x - p.x) / (p.next!!.y - p.y)
if (x <= hx && x > qx) {
qx = x
if (x == hx) {
if (hy == p.y) return p
if (hy == p.next!!.y) return p.next
}
m = if (p.x < p.next!!.x) p else p.next
}
}
p = p.next
} while (p !== outerNode)
if (m == null) return null
if (hx == qx) return m // hole touches outer segment; pick leftmost endpoint
// look for points inside the triangle of hole point, segment
// intersection and endpoint;
// if there are no points found, we have a valid connection;
// otherwise choose the point of the minimum angle with the ray as
// connection point
val stop: Node? = m
val mx = m.x
val my = m.y
var tanMin = Float.Companion.MAX_VALUE
var tan: Float
p = m
do {
if (hx >= p!!.x && p.x >= mx && pointInTriangle(
if (hy < my) hx else qx,
hy,
mx,
my,
if (hy < my) qx else hx,
hy,
p.x,
p.y
)
) {
tan = abs(hy - p.y) / (hx - p.x) // tangential
if (FloatEarcut.locallyInside(
p,
hole
) && (tan < tanMin || (tan == tanMin && (p.x > m!!.x || (p.x == m.x && FloatEarcut.sectorContainsSector(
m,
p
)))))
) {
m = p
tanMin = tan
}
}
p = p.next
} while (p !== stop)
return m
}
private fun locallyInside(a: Node, b: Node): Boolean {
return if (FloatEarcut.area(a.prev!!, a, a.next!!) < 0) FloatEarcut.area(
a,
b,
a.next!!
) >= 0 && FloatEarcut.area(a, a.prev!!, b) >= 0 else FloatEarcut.area(a, b, a.prev!!) < 0 || FloatEarcut.area(
a,
a.next!!,
b
) < 0
}
whether sector in vertex m contains sector in vertex p in the same coordinates
source — 121 lines, from line 679
private fun sectorContainsSector(m: Node, p: Node): Boolean {
return FloatEarcut.area(m.prev!!, m, p.prev!!) < 0 && FloatEarcut.area(p.next!!, m, m.next!!) < 0
}
private fun pointInTriangle(
ax: Float,
ay: Float,
bx: Float,
by: Float,
cx: Float,
cy: Float,
px: Float,
py: Float
): Boolean {
return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0
}
private fun getLeftmost(start: Node): Node {
var p: Node? = start
var leftmost: Node? = start
do {
if (p!!.x < leftmost!!.x || (p.x == leftmost.x && p.y < leftmost.y)) leftmost = p
p = p.next
} while (p !== start)
return leftmost
}
private fun linkedList(data: FloatArray, start: Int, end: Int, dim: Int, clockwise: Boolean): Node {
var last: Node? = null
if (clockwise == (signedArea(data, start, end, dim) > 0)) {
var i = start
while (i < end) {
last = insertNode(i, data[i], data[i + 1], last)
i += dim
}
} else {
var i = (end - dim)
while (i >= start) {
last = insertNode(i, data[i], data[i + 1], last)
i -= dim
}
}
if (last != null && FloatEarcut.equals(last, last.next!!)) {
removeNode(last)
last = last.next
}
return last!!
}
private fun removeNode(p: Node) {
p.next!!.prev = p.prev
p.prev!!.next = p.next
if (p.prevZ != null) {
p.prevZ!!.nextZ = p.nextZ
}
if (p.nextZ != null) {
p.nextZ!!.prevZ = p.prevZ
}
}
private fun insertNode(i: Int, x: Float, y: Float, last: Node?): Node {
val p = Node(i, x, y)
if (last == null) {
p.prev = p
p.next = p
} else {
p.next = last.next
p.prev = last
last.next!!.prev = p
last.next = p
}
return p
}
private fun signedArea(data: FloatArray, start: Int, end: Int, dim: Int): Float {
var sum = 0f
var j = end - dim
var i = start
while (i < end) {
sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1])
j = i
i += dim
}
return sum
}
private class Node(var i: Int, var x: Float, var y: Float) {
var z: Float
var steiner: Boolean = false
var prev: Node? = null
var next: Node? = null
var prevZ: Node? = null
var nextZ: Node? = null
init {
// vertice index in coordinates array
// vertex coordinates
// previous and next vertice nodes in a polygon ring
// z-order curve value
this.z = Float.Companion.MIN_VALUE
// previous and next nodes in z-order
// indicates whether this is a steiner point
}
override fun toString(): String {
val sb = StringBuilder()
sb.append("{i: ").append(i).append(", x: ").append(x).append(", y: ").append(y).append(", prev: ")
.append(prev).append(", next: ").append(next)
return sb.toString()
}
}
}