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.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