![JAR search and dependency download from the Maven repository](/logo.png)
commonMain.ru.casperix.math.geometry.builder.Triangulator.kt Maven / Gradle / Ivy
package ru.casperix.math.geometry.builder
import ru.casperix.math.collection.getLooped
import ru.casperix.math.curve.float32.Curve2f
import ru.casperix.math.geometry.*
import ru.casperix.math.geometry.float32.getWindingOrder
import ru.casperix.math.geometry.float32.normal
import ru.casperix.math.intersection.float32.Intersection2Float
import ru.casperix.math.straight_line.float32.LineSegment2f
import ru.casperix.math.vector.float32.Vector2f
import ru.casperix.math.vector.rotateCCW
import ru.casperix.math.vector.rotateCW
import ru.casperix.math.geometry.builder.PointCache.Companion.pointCache
import kotlin.math.max
object Triangulator {
/**
* Triangulate with "Ear clipping method"
*
* Don't supported self intersection edges
*/
fun polygon(polygon: Polygon2f): List {
val vertices = polygon.getVertices()
if (vertices.size < 3) {
//its line
return emptyList()
}
if (vertices.size == 3) {
return listOf(Triangle.from(vertices)!!)
}
val earTriangleList = mutableListOf()
val order = polygon.getWindingOrder()
generateEars(vertices, order, earTriangleList)
return earTriangleList
}
data class PolygonWithContour(val border: List, val body: List)
fun polygonWithContour(shape: Polygon2f, borderThick: Float, mode: BorderMode = BorderMode.CENTER): PolygonWithContour {
if (shape.getVertices().size < 3) {
return PolygonWithContour(emptyList(), polygon(shape))
}
val range = borderThick / 2f
val bigRange = when (mode) {
BorderMode.CENTER -> range
BorderMode.INSIDE -> 0f
BorderMode.OUTSIDE -> range * 2f
}
val smallRange = when (mode) {
BorderMode.CENTER -> -range
BorderMode.INSIDE -> -range * 2f
BorderMode.OUTSIDE -> 0f
}
val biggerPolygon = (if (bigRange != 0f) shape.growContour(bigRange) else shape)
val smallerPolygon = (if (smallRange != 0f) shape.growContour(smallRange) else shape)
val bigger = biggerPolygon.getVertices()
val smaller = smallerPolygon.getVertices()
val borderVertices = smaller + smaller.first() + bigger.first() + bigger.reversed()
val borderList = polygon(CustomPolygon(borderVertices))
return PolygonWithContour(borderList, polygon(smallerPolygon))
}
private fun Polygon2f.growContour(value: Float): Polygon2f {
val vertices = getVertices()
if (vertices.size <= 2) {
return this
}
val mainOrder = getWindingOrder()
val factor = value * (if (mainOrder == RotateDirection.COUNTERCLOCKWISE) -1f else 1f)
val isInsideGrow = value < 0f
return CustomPolygon(vertices.flatMapIndexed { index, current ->
val last = vertices.getLooped(index - 1)
val next = vertices.getLooped(index + 1)
val edgeA = Line(current, last)
val edgeB = Line(current, next)
val normalA = edgeA.delta().rotateCW().normalize()
val normalB = edgeB.delta().rotateCCW().normalize()
if ((normalA - normalB).length() < 0.01f) {
// if edge approximately parallel... use median normal
val normal = (normalA + normalB).normalize()
listOf(current + normal * factor)
} else {
// another case, search edge(with offset) intersection
val lineA = edgeA.convert { it + normalA * factor }
val lineB = edgeB.convert { it + normalB * factor }
val currentOrder = getWindingOrder(vertices, index)
if ((isInsideGrow && currentOrder == mainOrder) || (!isInsideGrow && currentOrder != mainOrder)) {
val candidate = Intersection2Float.getLineWithLine(lineA, lineB)
val point = candidate ?: edgeA.v0
listOf(point)
} else {
val normal = (normalA + normalB).normalize()
listOf(current + normalA * factor, current + normal * factor, current + normalB * factor)
}
}
})
}
private fun generateEars(vertices: List, mainOrder:RotateDirection, earList: MutableList>) {
vertices.forEachIndexed { index, current ->
val last = vertices.getLooped(index - 1)
val next = vertices.getLooped(index + 1)
val triangle = Triangle2f(last, next, current)
val otherVertices = vertices.toSet() - setOf(last, next, current)
val intersection = otherVertices.firstOrNull { other ->
hasPointWithTriangle2(other, triangle)
}
val vertexOrder = getWindingOrder(vertices, index)
if (mainOrder == vertexOrder && intersection == null) {
earList += triangle
val nextVertices = vertices.toMutableList()
nextVertices.removeAt(index)
generateEars(nextVertices, mainOrder, earList)
return
}
}
}
fun hasPointWithTriangle2(P: Vector2f, triangle: Triangle2f): Boolean {
// Compute vectors
val v0 = triangle.v2 - triangle.v0
val v1 = triangle.v1 - triangle.v0
val v2 = P - triangle.v0
// Compute dot products
val dot00 = v0.dot(v0)
val dot01 = v0.dot(v1)
val dot02 = v0.dot(v2)
val dot11 = v1.dot(v1)
val dot12 = v1.dot(v2)
// Compute barycentric coordinates
val invDenom = 1.0 / (dot00 * dot11 - dot01 * dot01)
val u = (dot11 * dot02 - dot01 * dot12) * invDenom
val v = (dot00 * dot12 - dot01 * dot02) * invDenom
// Check if point is in triangle
return (u > 0) && (v > 0) && (u + v < 1.0)
}
fun line(line: Line2f, thick: Float): List {
val left = line.normal() * thick / 2f
return quad(
Quad2f(
line.v0 - left,
line.v1 - left,
line.v1 + left,
line.v0 + left,
)
)
}
fun segment(segment: LineSegment2f, thick: Float): List {
val left = segment.normal() * thick / 2f
return quad(
Quad2f(
segment.start - left,
segment.finish - left,
segment.finish + left,
segment.start + left,
)
)
}
fun quad(quad: Quad2f): List {
return quad.run {
listOf(Triangle2f(v0, v1, v2), Triangle2f(v0, v2, v3))
}
}
fun circle(center: Vector2f, rangeInside: Float, rangeOutside: Float, steps: Int = 64): List {
val points = pointCache.getOrPut(steps) { PointCache(steps) }.points
return (0 until points.size - 1).flatMap { index ->
quad(
Quad2f(
points[index] * rangeInside + center,
points[index + 1] * rangeInside + center,
points[index + 1] * rangeOutside + center,
points[index] * rangeOutside + center,
)
)
}
}
fun arrow(
curve: Curve2f,
lineThick: Float,
arrowMode: ArrowMode = UniformArrowMode(),
parts: Int = 100,
): List {
val lineRange = lineThick / 2f
val arrowRange = if (arrowMode is ProportionalArrowMode) {
arrowMode.thickFactor * lineRange
} else if (arrowMode is FixedSizeArrowMode) {
arrowMode.maxThick / 2f
} else if (arrowMode is UniformArrowMode) {
arrowMode.maxThick / 2f
} else {
throw Exception("invalid arrow mode: $arrowMode")
}
val tStart = if (arrowMode is ProportionalArrowMode) {
1f - arrowMode.lengthFactor
} else if (arrowMode is FixedSizeArrowMode) {
1f - arrowMode.length / curve.length()
} else if (arrowMode is UniformArrowMode) {
max(1f - arrowMode.maxLengthFactor, 1f - arrowMode.length / curve.length())
} else {
throw Exception("invalid arrow mode: $arrowMode")
}.coerceIn(0f, 1f)
val (line, arrow) = curve.divide(tStart)
val lineParts = max(1, (tStart * parts).toInt())
val arrowParts = max(1, parts - lineParts)
return curve(line, lineThick, lineParts) + selfArrow(arrow, arrowRange, arrowParts)
}
fun selfArrow(curve: Curve2f, arrowRange: Float, arrowParts: Int): List {
val pointPairs = (0..arrowParts).map {
val t = it.toFloat() / arrowParts
val arrowScale = 1f - t
val pivot = curve.getPosition(t)
val left = curve.getNormal(t) * arrowScale * arrowRange
LineSegment2f(pivot + left, pivot - left)
}
return strip(pointPairs)
}
fun curve(
curve: Curve2f,
thick: Float,
parts: Int = 100
): List {
val range = thick / 2f
val sections = (0..parts).map {
val t = it.toFloat() / parts
val pivot = curve.getPosition(t)
val left = curve.getNormal(t) * range
LineSegment2f(pivot + left, pivot - left)
}
return strip(sections)
}
/**
* sections[0-1-2...].start
* |
* |---strip--->
* |
* sections[0-1-2...].finish
*/
fun strip(sections: List): List {
return (0 until sections.size - 1).flatMap { partId ->
val (A, B) = sections[partId]
val (D, C) = sections[partId + 1]
quad(Quad2f(A, B, C, D))
}
}
fun point(center: Vector2f, diameter: Float, steps: Int = 16): List {
val points = pointCache.getOrPut(steps) { PointCache(steps) }.points
val range = diameter / 2f
return (0 until points.size - 1).map { index ->
val last = points[index] * range + center
val next = points[index + 1] * range + center
Triangle2f(center, last, next)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy