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

commonMain.utils.Intersections.kt Maven / Gradle / Ivy

There is a newer version: 0.4.5-alpha6
Show newest version
package org.openrndr.kartifex.utils

import org.openrndr.kartifex.*
import kotlin.jvm.JvmName
import kotlin.math.*

@JvmName("kartifexHackSort1")
fun  Array.sort(start: Int = 0, end: Int = size, selector: (T) -> Double) {
    if (start != 0 || end != size) {
        val copy = copyOfRange(start, end)
        copy.sortBy(selector)
        copy.copyInto(this, start)
    } else {
        sortBy(selector)
    }
}

@JvmName("kartifexHackSort2")
fun  sort(a: Array, start: Int = 0, end: Int = a.size, selector: (T) -> Double) {
    a.sort(start, end, selector)
}


object Intersections {
    // utilities
    const val FAT_LINE_PARAMETRIC_RESOLUTION = 1e-5
    const val FAT_LINE_SPATIAL_EPSILON = 1e-5

    const val PARAMETRIC_EPSILON = 1e-5
    const val SPATIAL_EPSILON = 1e-5

    const val MAX_CUBIC_CUBIC_INTERSECTIONS = 9

    val PARAMETRIC_BOUNDS: Box2 =
        Box.box(Vec2(0.0, 0.0), Vec2(1.0, 1.0))

    fun lineCurve(a: Line2, b: Curve2): Array {
        return if (b is Line2) {
            lineLine(a, b)
        } else if (b.isFlat(SPATIAL_EPSILON)) {
            lineLine(a, Line2.line(b.start(), b.end()))
        } else if (b is Bezier2.QuadraticBezier2) {
            lineQuadratic(a, b)
        } else {
            lineCubic(a, b as Bezier2.CubicBezier2)
        }
    }

    fun lineLine(a: Line2, b: Line2): Array {
        val av: Vec2 = a.end().sub(a.start())
        val bv: Vec2 = b.end().sub(b.start())
        val d: Double = Vec2.cross(av, bv)
        if (abs(d) < 1e-6) {
            val ints: Array = collinearIntersection(a, b)
            if (ints.all { v: Vec2 ->
                    Vec.equals(
                        a.position(v.x),
                        b.position(v.y),
                        SPATIAL_EPSILON
                    )
                }
            ) {
                return ints
            } else if (abs(d) == 0.0) {
                return emptyArray()
            }
        }
        val asb = a.start().sub(b.start())
        val s = Vec2.cross(bv, asb) / d
        val t = Vec2.cross(av, asb) / d
        return arrayOf(Vec2(s, t))
    }

    fun lineQuadratic(
        p: Line2,
        q: Bezier2.QuadraticBezier2
    ): Array {
        // (p0 - 2p1 + p2) t^2 + (-2p0 + 2p1) t + p0
        val a: Vec2 = q.p0.add(q.p1.mul(-2.0)).add(q.p2)
        val b: Vec2 = q.p0.mul(-2.0).add(q.p1.mul(2.0))
        val c: Vec2 = q.p0
        val dir: Vec2 = p.end().sub(p.start())
        val n = Vec2(-dir.y, dir.x)
        val roots: DoubleArray = Equations.solveQuadratic(
            Vec.dot(n, a),
            Vec.dot(n, b),
            Vec.dot(n, c) + Vec2.cross(p.start(), p.end())
        )
        val result: Array = if (Scalars.equals(dir.x, 0.0, Scalars.EPSILON)) {
            val y0: Double = p.start().y
            Array(roots.size) { i ->
                val t = roots[i]
                val y1: Double = q.position(t).y
                Vec2((y1 - y0) / dir.y, t)
            }
        } else {
            val x0: Double = p.start().x
            Array(roots.size) { i ->
                val t = roots[i]
                val x1: Double = q.position(t).x
                Vec2((x1 - x0) / dir.x, t)
            }
        }
        return result
    }

    fun lineCubic(
        p: Line2,
        q: Bezier2.CubicBezier2
    ): Array {
        // (-p0 + 3p1 - 3p2 + p3) t^3 + (3p0 - 6p1 + 3p2) t^2 + (-3p0 + 3p1) t + p0
        val a: Vec2 = q.p0.mul(-1.0).add(q.p1.mul(3.0)).add(q.p2.mul(-3.0)).add(q.p3)
        val b: Vec2 = q.p0.mul(3.0).add(q.p1.mul(-6.0)).add(q.p2.mul(3.0))
        val c: Vec2 = q.p0.mul(-3.0).add(q.p1.mul(3.0))
        val d: Vec2 = q.p0
        val dir: Vec2 = p.end().sub(p.start())
        val dLen: Double = dir.length()
        val n = Vec2(-dir.y, dir.x)
        val roots: DoubleArray = Equations.solveCubic(
            Vec.dot(n, a),
            Vec.dot(n, b),
            Vec.dot(n, c),
            Vec.dot(n, d) + Vec2.cross(p.start(), p.end())
        )
        val result = Array(roots.size) { i ->
            val t = roots[i]
            val v = q.position(t).sub(p.start())
            val vLen: Double = v.length()
            val s: Double = vLen / dLen * signum(Vec.dot(dir, v))
            Vec2(s, t)
        }
        return result
    }

    fun normalize(intersections: Array): Array {
        var limit = intersections.size
        if (limit == 0) {
            return intersections
        }
        var readIdx: Int
        var writeIdx: Int

        // round and filter within [0, 1]
        readIdx = 0
        writeIdx = 0
        while (readIdx < limit) {
            val i: Vec2 = intersections[readIdx].map { n: Double ->
                round(
                    n,
                    PARAMETRIC_EPSILON
                )
            }
            if (PARAMETRIC_BOUNDS.contains(i)) {
                intersections[writeIdx++] = i
            }
            readIdx++
        }
        limit = writeIdx
        if (limit > 1) {
            // dedupe intersections on b
            //intersections.sortBy { it.y }
            sort(
                intersections,
                0,
                limit,
                { v: Vec2 -> v.y }
            )
            readIdx = 0
            writeIdx = -1
            while (readIdx < limit) {
                val i: Vec2 = intersections[readIdx]
                if (writeIdx < 0 || !Scalars.equals(intersections[writeIdx].y, i.y, Scalars.EPSILON)) {
                    intersections[++writeIdx] = i
                }
                readIdx++
            }
            limit = writeIdx + 1
        }
        if (limit > 1) {
            // dedupe intersections on a
            sort(
                intersections,
                0,
                limit,
                { v: Vec2 -> v.x })

            readIdx = 0
            writeIdx = -1
            while (readIdx < limit) {
                val i: Vec2 = intersections[readIdx]
                if (writeIdx < 0 || !Scalars.equals(intersections[writeIdx].x, i.x, Scalars.EPSILON)) {
                    intersections[++writeIdx] = i
                }
                readIdx++
            }
            limit = writeIdx + 1
        }
        val result: Array = Array(limit) {
            intersections[it]
        }
        return result
    }

    // analytical methods
    fun collinearIntersection(
        a: Curve2,
        b: Curve2
    ): Array {
        val result = mutableListOf()
        for (i in 0..1) {
            val tb: Double = b.nearestPoint(a.position(i.toDouble()))

            // a overhangs the start of b
            if (tb <= 0) {
                val s: Double = round(
                    a.nearestPoint(b.start()),
                    PARAMETRIC_EPSILON
                )
                if (0.0 <= s && s <= 1.0) {
                    result.add(Vec2(s, 0.0))
                }

                // a overhangs the end of b
            } else if (tb >= 1) {
                val s: Double = round(
                    a.nearestPoint(b.end()),
                    PARAMETRIC_EPSILON
                )
                if (0 <= s && s <= 1) {
                    result.add(Vec2(s, 1.0))
                }

                // a is contained in b
            } else {
                result.add(Vec2(i.toDouble(), tb))
            }
        }
        if (result.size == 2 && Vec.equals(
                result[0],
                result[1],
                PARAMETRIC_EPSILON
            )
        ) {
            result.removeLast()
        }
        return result.toTypedArray()
    }

    // subdivision (slow, but as close to a reference implementation as exists)
    class CurveInterval(
        curve: Curve2,
        tLo: Double,
        tHi: Double,
        pLo: Vec2,
        pHi: Vec2
    ) {
        val curve: Curve2
        val isFlat: Boolean
        val tLo: Double
        val tHi: Double
        val pLo: Vec2
        val pHi: Vec2
        fun bounds(): Box2 {
            return Box.box(pLo, pHi)
        }

        fun intersects(c: CurveInterval): Boolean {
            return bounds().expand(SPATIAL_EPSILON).intersects(c.bounds())
        }

        fun split(): Array {
            return if (isFlat) {
                arrayOf(this)
            } else {
                val tMid = (tLo + tHi) / 2
                val pMid: Vec2 = curve.position(tMid)
                arrayOf(
                    CurveInterval(curve, tLo, tMid, pLo, pMid),
                    CurveInterval(curve, tMid, tHi, pMid, pHi)
                )
            }
        }

        fun intersections(c: CurveInterval, acc: MutableList) {
            for (i in lineLine(Line2.line(pLo, pHi), Line2.line(c.pLo, c.pHi))) {
                if (PARAMETRIC_BOUNDS.expand(PARAMETRIC_EPSILON).contains(i)) {
                    acc.add(Vec.lerp(Vec2(tLo, c.tLo), Vec2(tHi, c.tHi), i))
                }
            }
        }

        override fun toString(): String {
            return "[$tLo, $tHi]"
        }

        companion object {
            fun from(c: Curve2): Array {
                val ts: DoubleArray = c.inflections()
                ts.sort()
                return if (ts.size == 0) {
                    arrayOf(CurveInterval(c, 0.0, 1.0, c.start(), c.end()))
                } else {
                    val ls = arrayOfNulls(ts.size + 1)
                    for (i in ls.indices) {
                        val lo: Double = if (i == 0) 0.0 else ts[i - 1]
                        val hi: Double = if (i == ls.size - 1) 1.0 else ts[i]
                        ls[i] = CurveInterval(c, lo, hi, c.position(lo), c.position(hi))
                    }
                    ls
                }
            }
        }

        init {
            this.curve = curve
            this.tLo = tLo
            this.tHi = tHi
            this.pLo = pLo
            this.pHi = pHi
            isFlat = (Vec.equals(pLo, pHi, SPATIAL_EPSILON)
                    || tHi - tLo < PARAMETRIC_EPSILON || curve.range(tLo, tHi)
                .isFlat(SPATIAL_EPSILON))
        }
    }


    // post-processing
    fun round(n: Double, epsilon: Double): Double {
        return if (Scalars.equals(n, 0.0, epsilon)) {
            0.0
        } else if (Scalars.equals(n, 1.0, epsilon)) {
            1.0
        } else {
            n
        }
    }

    //
    fun intersections(a: Curve2, b: Curve2): Array {
        if (!a.bounds().expand(SPATIAL_EPSILON).intersects(b.bounds())) {
            return emptyArray()
        }
        return if (a is Line2) {
            normalize(lineCurve(a, b))
        } else if (b is Line2) {
            val result: Array = normalize(
                lineCurve(b, a)
            )
            for (i in result.indices) {
                result[i] = result[i].swap()
            }
            result
        } else {
            //return subdivisionCurveCurve(a, b);
            fatLineCurveCurve(a, b)
        }
    }

    // fatline

    // fat lines (faster, but more temperamental)
    // This is adapted from Sederberg's "Curve Intersection Using Bezier Clipping", but the algorithm as described
    // gets unstable when one curve is clipped small enough, causing it to over-clip the other curve, causing us to miss
    // intersection points.  To address this, we quantize the curve sub-ranges using FAT_LINE_PARAMETRIC_RESOLUTION,
    // preventing them from getting too small, and expand the width of our clipping regions by FAT_LINE_SPATIAL_EPSILON.
    fun signedDistance(p: Vec2, a: Vec2, b: Vec2): Double {
        val d: Vec2 = b.sub(a)
        return (Vec2.cross(p, d) + Vec2.cross(b, a)) / d.length()
    }

    fun fatLineWidth(c: Curve2): Interval {
        return if (c is Line2) {
            Interval.interval(0.0, 0.0)
        } else if (c is Bezier2.QuadraticBezier2) {
            val b: Bezier2.QuadraticBezier2 = c
            Interval.interval(0.0, signedDistance(b.p1, b.p0, b.p2) / 2)
        } else if (c is Bezier2.CubicBezier2) {
            val b: Bezier2.CubicBezier2 = c
            val d1 = signedDistance(b.p1, b.p0, b.p3)
            val d2 = signedDistance(b.p2, b.p0, b.p3)
            val k = if (d1 * d2 < 0) 4 / 9.0 else 3 / 4.0
            Interval.interval(
                min(
                    0.0,
                    min(d1, d2)
                ) * k,
                max(0.0, max(d1, d2)) * k
            )
        } else {
            throw IllegalStateException()
        }
    }

    fun convexHull(
        a: Vec2,
        b: Vec2,
        c: Bezier2.QuadraticBezier2
    ): Array {
        val p0 = Vec2(0.0, signedDistance(c.p0, a, b))
        val p1 = Vec2(1 / 2.0, signedDistance(c.p1, a, b))
        val p2 = Vec2(1.0, signedDistance(c.p2, a, b))
        return arrayOf(p0, p1, p2, p0)
    }

    fun convexHull(
        a: Vec2,
        b: Vec2,
        c: Bezier2.CubicBezier2
    ): Array {
        val p0 = Vec2(0.0, signedDistance(c.p0, a, b))
        val p1 = Vec2(1 / 3.0, signedDistance(c.p1, a, b))
        val p2 = Vec2(2 / 3.0, signedDistance(c.p2, a, b))
        val p3 = Vec2(1.0, signedDistance(c.p3, a, b))
        val d1 = signedDistance(p1, p0, p3)
        val d2 = signedDistance(p2, p0, p3)
        return if (d1 * d2 < 0) {
            arrayOf(p0, p1, p3, p2, p0)
        } else {
            val k = d1 / d2
            if (k >= 2) {
                arrayOf(p0, p1, p3, p0)
            } else if (k <= 0.5) {
                arrayOf(p0, p2, p3, p0)
            } else {
                arrayOf(p0, p1, p2, p3, p0)
            }
        }
    }

    fun convexHull(
        a: Vec2,
        b: Vec2,
        c: Curve2
    ): Array {
        return when (c) {
            is Bezier2.QuadraticBezier2 -> {
                convexHull(a, b, c)
            }
            is Bezier2.CubicBezier2 -> {
                convexHull(a, b, c)
            }
            else -> {
                throw IllegalStateException()
            }
        }
    }

    fun clipHull(fatLine: Interval, hull: Array): Interval {
        var lo = Double.POSITIVE_INFINITY
        var hi = Double.NEGATIVE_INFINITY
        for (i in 0 until hull.size - 1) {
            if (fatLine.contains(hull[i].y)) {
                lo = min(lo, hull[i].x)
                hi = max(hi, hull[i].x)
            }
        }
        for (y in doubleArrayOf(fatLine.lo, fatLine.hi)) {
            for (i in 0 until hull.size - 1) {
                val a: Vec2 = hull[i]
                val b: Vec2 = hull[i + 1]
                if (Interval.interval(a.y, b.y).contains(y)) {
                    if (a.y == b.y) {
                        lo = min(lo, min(a.x, b.x))
                        hi = max(
                            lo, max(a.x, b.x)
                        )
                    } else {
                        val t = Scalars.lerp(a.x, b.x, (y - a.y) / (b.y - a.y))
                        lo = min(lo, t)
                        hi = max(hi, t)
                    }
                }
            }
        }
        return if (hi < lo) {
            Interval.EMPTY
        } else {
            Interval.interval(lo, hi)
        }
    }

    fun quantize(t: Interval): Interval {
        val resolution: Double = FAT_LINE_PARAMETRIC_RESOLUTION
        val lo: Double = min(
            1 - resolution,
            floor(t.lo / resolution) * resolution
        )
        val hi: Double = max(
            lo + resolution,
            ceil(t.hi / resolution) * resolution
        )
        return Interval.interval(lo, hi)
    }

    fun addIntersections(
        a: FatLine,
        b: FatLine,
        acc: MutableList
    ) {
        val la: Line2 = a.line()
        val lb: Line2 = b.line()
        val av: Vec2 = la.end().sub(la.start())
        val bv: Vec2 = lb.end().sub(lb.start())
        val asb: Vec2 = la.start().sub(lb.start())
        val d: Double = Vec2.cross(av, bv)
        val i = Vec2(
            Vec2.cross(bv, asb) / d,
            Vec2.cross(av, asb) / d
        )
        if (PARAMETRIC_BOUNDS.expand(0.1).contains(i)) {
            acc.add(Box.box(a.t, b.t).lerp(i))
        }
    }

    fun clipFatline(
        subject: FatLine,
        clipper: FatLine
    ): FatLine? {
        val hull = convexHull(clipper.range.start(), clipper.range.end(), subject.range)

        val expanded = clipper._line.expand(FAT_LINE_SPATIAL_EPSILON)
        val normalized = clipHull(expanded, hull)
        return if (normalized.isEmpty) null else FatLine(
            subject.curve,
            subject.t.lerp(normalized)
        )
    }

    class FatLine internal constructor(curve: Curve2, t: Interval) {
        val curve: Curve2
        val range: Curve2
        val t: Interval
        val _line: Interval
        fun mid(): Double {
            return t.lerp(0.5)
        }

        val isFlat: Boolean
            get() = t.size() < PARAMETRIC_EPSILON || _line.size() <= SPATIAL_EPSILON

        fun bounds(): Box2 {
            return Box.box(range.start(), range.end())
        }

        fun intersects(l: FatLine): Boolean {
            return bounds().expand(SPATIAL_EPSILON * 10).intersects(l.bounds())
        }

        fun split(): Array {
            return if (isFlat) {
                arrayOf(this)
            } else arrayOf(
                FatLine(curve, Interval.interval(t.lo, mid())),
                FatLine(curve, Interval.interval(mid(), t.hi))
            )
        }

        fun line(): Line2 {
            return Line2.line(range.start(), range.end())
        }

        override fun toString(): String {
            return "FatLine(curve=$curve, range=$range, t=$t, _line=$_line, isFlat=$isFlat)"
        }


        companion object {
            fun from(c: Curve2): Array {
                val ts: DoubleArray = c.inflections()
                ts.sort()
                return if (ts.isEmpty()) {
                    arrayOf(FatLine(c, Interval.interval(0.0, 1.0)))
                } else {
                    val result = arrayOfNulls(ts.size + 1)
                    for (i in result.indices) {
                        val lo: Double = if (i == 0) 0.0 else ts[i - 1]
                        val hi: Double = if (i == result.size - 1) 1.0 else ts[i]
                        result[i] = FatLine(c, Interval.interval(lo, hi))
                    }
                    @Suppress("UNCHECKED_CAST")
                    result as Array
                }
            }
        }

        init {
            this.curve = curve
            this.t = quantize(t)
            range = curve.range(this.t)
            _line = fatLineWidth(range)
        }
    }

//    fun fatLineWidth(c: Curve2): Interval? {
//        return if (c is Line2) {
//            Interval.interval(0.0, 0.0)
//        } else if (c is Bezier2.QuadraticBezier2) {
//            val b: Bezier2.QuadraticBezier2 = c as Bezier2.QuadraticBezier2
//            Interval.interval(
//                0.0,
//                Intersections.signedDistance(b.p1, b.p0, b.p2) / 2
//            )
//        } else if (c is Bezier2.CubicBezier2) {
//            val b: Bezier2.CubicBezier2 = c as Bezier2.CubicBezier2
//            val d1: Double = Intersections.signedDistance(b.p1, b.p0, b.p3)
//            val d2: Double = Intersections.signedDistance(b.p2, b.p0, b.p3)
//            val k = if (d1 * d2 < 0) 4 / 9.0 else 3 / 4.0
//            Interval.interval(
//                min(
//                    0.0,
//                    min(d1, d2)
//                ) * k,
//                max(0.0, max(d1, d2)) * k
//            )
//        } else {
//            throw IllegalStateException()
//        }
//    }

//    fun quantize(t: Interval): Interval? {
//        val resolution: Double = Intersections.FAT_LINE_PARAMETRIC_RESOLUTION
//        val lo: Double = min(
//            1 - resolution,
//            floor(t.lo / resolution) * resolution
//        )
//        val hi: Double = max(
//            lo + resolution,
//            ceil(t.hi / resolution) * resolution
//        )
//        return Interval.interval(lo, hi)
//    }

    fun fatLineCurveCurve(a: Curve2, b: Curve2): Array {
        val queue = ArrayDeque()
        val `as` = FatLine.from(a)
        val bs = FatLine.from(b)
        for (ap in `as`) {
            for (bp in bs) {
                queue.apply {
                    addLast(ap)
                    addLast(bp)
                }
            }
        }
        var iterations = 0
        var collinearCheck = false
        val acc = ArrayDeque()
        while (queue.size > 0) {
            // if it's taking a while, check once (and only once) if they're collinear
            if (iterations > 32 && !collinearCheck) {
                collinearCheck = true
                val `is`: Array =
                    collinearIntersection(a, b)
                if (isCollinear(a, b, `is`)) {
                    return `is`
                }
            }
            var lb = queue.removeLast()
            var la = queue.removeLast()
            while (true) {
                iterations++
                if (!la.intersects(lb)) {
                    break
                }
                if (la.isFlat && lb.isFlat) {
                    addIntersections(la, lb, acc)
                    break
                }
                val aSize: Double = la.t.size()
                val bSize: Double = lb.t.size()

                // use a to clip b
                val lbPrime = clipFatline(lb, la) ?: break
                lb = lbPrime

                // use b to clip a
                val laPrime: FatLine = clipFatline(la, lb) ?: break
                la = laPrime
                val ka: Double = la.t.size() / aSize
                val kb: Double = lb.t.size() / bSize
                if (max(ka, kb) > 0.8) {
                    // TODO: switch over to subdivision at some point?
                    for (ap in la.split()) {
                        for (bp in lb.split()) {
                            queue.apply {
                                addLast(ap)
                                addLast(bp)
                            }
                        }
                    }
                    break
                }
            }
        }
        return normalize(acc.toTypedArray())
    }

    private fun isCollinear(
        a: Curve2,
        b: Curve2,
        `is`: Array
    ): Boolean {
        if (`is`.size != 2) {
            return false
        }
        for (i in 0 until MAX_CUBIC_CUBIC_INTERSECTIONS + 1) {
            val t: Double = i.toDouble() / MAX_CUBIC_CUBIC_INTERSECTIONS
            val pa: Vec2 = a.position(Scalars.lerp(`is`[0].x, `is`[1].x, t))
            val pb: Vec2 = b.position(Scalars.lerp(`is`[0].y, `is`[1].y, t))
            if (!Vec.equals(
                    pa,
                    pb,
                    SPATIAL_EPSILON
                )
            ) {
                return false
            }
        }
        return true
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy