All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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