
commonMain.ru.casperix.math.geometry.builder.Triangulator.kt Maven / Gradle / Ivy
package ru.casperix.math.geometry.builder
import ru.casperix.math.angle.float32.RadianFloat
import ru.casperix.math.axis_aligned.float32.Box2f
import ru.casperix.math.collection.getLooped
import ru.casperix.math.curve.float32.Curve2f
import ru.casperix.math.geometry.*
import ru.casperix.math.geometry.builder.PointCache.Companion.pointCache
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.vector.toQuad
import kotlin.math.max
import kotlin.math.roundToInt
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 intersection = (0..index - 2).firstOrNull {
hasPointWithTriangle2(vertices[it], triangle)
} ?: (index + 2..vertices.lastIndex).firstOrNull {
hasPointWithTriangle2(vertices[it], 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)
}
}
fun arc(
center: Vector2f,
rangeInside: Float,
rangeOutside: Float,
parts: Int,
startAngle: RadianFloat,
finishAngle: RadianFloat
): List {
return (0 until parts).flatMap {
val w1 = it / parts.toFloat()
val w2 = (it + 1) / parts.toFloat()
val angleA = startAngle * w1 + finishAngle * (1f - w1)
val angleB = startAngle * w2 + finishAngle * (1f - w2)
val p0 = center + angleA.toDirection() * rangeInside
val p1 = center + angleA.toDirection() * rangeOutside
val p2 = center + angleB.toDirection() * rangeOutside
val p3 = center + angleB.toDirection() * rangeInside
quad(Quad2f(p0, p1, p2, p3))
}
}
fun roundRect(area: Box2f, cornerRange: Float): List {
return roundRect(area, cornerRange, cornerRange, cornerRange, cornerRange)
}
private fun getPartsByRange(range:Float):Int = (range * 0.5f).roundToInt().coerceIn(3, 64)
fun roundRect(
area: Box2f,
leftTopRange: Float,
rightTopRange: Float,
rightBottomRange: Float,
leftBottomRange: Float
): List {
if (leftTopRange <= 0f && rightTopRange <= 0f && rightBottomRange <= 0f && leftBottomRange <= 0f) {
return quad(area.toQuad())
}
val corners = area.toQuad()
val leftTopParts = getPartsByRange(leftTopRange)
val leftBottomParts = getPartsByRange(leftBottomRange)
val rightTopParts = getPartsByRange(rightTopRange)
val rightBottomParts = getPartsByRange(rightBottomRange)
/**
* c0--s1----s2--c1
* s8--q0----q1--s3
* | |
* | |
* s7--q3----q2--s4
* c3--s6----s5--c2
*/
val c0 = corners.v0
val c1 = corners.v1
val c2 = corners.v2
val c3 = corners.v3
val q0 = corners.v0 + Vector2f(leftTopRange, leftTopRange)
val q1 = corners.v1 + Vector2f(-rightTopRange, rightTopRange)
val q2 = corners.v2 + Vector2f(-rightBottomRange, -rightBottomRange)
val q3 = corners.v3 + Vector2f(leftBottomRange, -leftBottomRange)
val s1 = Vector2f(q0.x, c0.y)
val s2 = Vector2f(q1.x, c1.y)
val s3 = Vector2f(c1.x, q1.y)
val s4 = Vector2f(c2.x, q2.y)
val s5 = Vector2f(q2.x, c2.y)
val s6 = Vector2f(q3.x, c3.y)
val s7 = Vector2f(c3.x, q3.y)
val s8 = Vector2f(c0.x, q0.y)
return quad(Quad2f(q0, q1, q2, q3)) +
quad(Quad2f(s1, s2, q1, q0)) +
quad(Quad2f(s3, s4, q2, q1)) +
quad(Quad2f(s5, s6, q3, q2)) +
quad(Quad2f(s7, s8, q0, q3)) +
arc(q0, 0f, leftTopRange, leftTopParts, RadianFloat(fPI), RadianFloat(fPI + fHPI)) +
arc(q1, 0f, rightTopRange, rightTopParts, RadianFloat(-fHPI), RadianFloat(0f)) +
arc(q2, 0f, rightBottomRange, rightBottomParts, RadianFloat(0f), RadianFloat(fHPI)) +
arc(q3, 0f, leftBottomRange, leftBottomParts, RadianFloat(fHPI), RadianFloat(fPI))
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy