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

commonMain.utils.regions.Clip.kt Maven / Gradle / Ivy

package org.openrndr.kartifex.utils.regions

import org.openrndr.kartifex.Curve2
import org.openrndr.kartifex.Region2
import org.openrndr.kartifex.Ring2
import org.openrndr.kartifex.Vec2
import org.openrndr.kartifex.utils.Combinatorics
import org.openrndr.kartifex.utils.graphs.DirectedGraph
import org.openrndr.kartifex.utils.graphs.Graphs
import kotlin.math.E
import kotlin.math.abs

object Clip {
    // The approach used here is described at https://ideolalia.com/2018/08/28/artifex.html.  The "simplest" approach would
    // be to represent the unused segments as a multi-graph (since there can be multiple segments connecting any pair of vertices),
    // but the graph data structure used here is *not* a multi-graph, so instead we model it as a graph which only includes
    // the shortest edge between the vertices, and we just iterate over it multiple times.  Empirically 2-3 times should
    // always suffice, but we give ourselves a bit of breathing room because mostly we just want to preclude an infinite loop.
    private const val MAX_REPAIR_ATTEMPTS = 10

    private enum class Operation {
        UNION, INTERSECTION, DIFFERENCE
    }

    private enum class Type {
        OUTSIDE, INSIDE, SAME_EDGE, DIFF_EDGE
    }

    private fun operation(
        ra: Region2,
        rb: Region2,
        operation: Operation,
        aPredicate: (Type) -> Boolean,
        bPredicate: (Type) -> Boolean
    ): Region2 {
        val split: Split.Result = Split.split(ra, rb)
        val a: Region2 = split.a
        val b: Region2 = split.b

        // Partition rings into arcs separated at intersection points
        val pa: List = partition(a, split.splits)
        var pb: List = partition(b, split.splits)
        if (operation == Operation.DIFFERENCE) {
            pb = pb.map { obj: Arc -> obj.reverse() }
        }

        // Filter out arcs which are to be ignored, per our operation
        var arcs: MutableSet = mutableSetOf()
        pa.filter { arc: Arc -> aPredicate(classify(b, arc)) }.forEach { value -> arcs.add(value) }
        pb.filter { arc: Arc -> bPredicate(classify(a, arc)) }.forEach { value -> arcs.add(value) }

        /*
    describe("split", split.splits.elements());
    describe("arcs", arcs.elements().stream().map(Arc::vertices).toArray(IList[]::new));
    VERTICES.forEach(v -> System.out.println(VERTICES.indexOf(v) + " " + v));
    // */
        val result = mutableListOf()
        val consumed = mutableSetOf()

        // First we're going to extract complete cycles, and then try to iteratively repair the graph
        for (i in 0 until MAX_REPAIR_ATTEMPTS) {
            // Construct a graph where the edges are the set of all arcs connecting the vertices
            val graph = DirectedGraph>()
            arcs.forEach { arc: Arc ->
                graph.link(
                    arc.head(), arc.tail(), mutableSetOf(arc)
                ) { obj, s -> obj.union(s) }
            }

            //graph.vertices().forEach(v -> System.out.println(VERTICES.indexOf(v) + " " + graph.out(v).stream().map(VERTICES::indexOf).collect(Lists.linearCollector())));
            if (i > 0) {
                for (path in repairGraph(
                    graph,
                    (pa + pb) - arcs - consumed
                )) {
                    for (arc in path) {
                        // if the graph currently contains the arc, remove it
                        if (arcs.contains(arc)) {
                            //describe("remove", arc.vertices());
                            graph.unlink(arc.head(), arc.tail())
                            arcs.remove(arc)

                            // if the graph doesn't contain the arc, add it
                        } else {
                            //describe("add", arc.vertices());
                            graph.link(arc.head(), arc.tail(), mutableSetOf(arc))
                            arcs.add(arc)
                        }
                    }
                }
            }

            // find every cycle in the graph, and then expand those cycles into every possible arc combination, yielding a bunch
            // of rings ordered from largest to smallest
            val cycles: List> = Graphs.cycles(graph)
                .map { cycle ->
                    edges(cycle
                    ) { x, y -> graph.edge(x, y).toList() }
                }
                .map { paths -> Combinatorics.combinations(paths) }
                .flatten()
                .sortedBy { area -> area(area) }
                .reversed()

            // extract as many cycles as possible without using the same arc twice
            for (cycle in cycles) {
                //describe("cycle", cycle.stream().map(Arc::vertices).toArray(IList[]::new));
                if (cycle.any { value -> consumed.contains(value) }
                ) {
                    continue
                }
                cycle.forEach { value -> consumed.add(value) }
                result.add(ring(cycle))
            }
            arcs = (arcs - consumed).toMutableSet()
            if (arcs.size == 0) {
                break
            }
        }
        //assert(arcs.size() == 0L)
        return Region2(result)
    }

    private fun isTop(c: Curve2): Boolean {
//        if (c == null) {
//            return false
//        }
        val delta: Double = c.end().x - c.start().x
        return if (delta == 0.0) {
            c.end().y > c.start().y
        } else delta < 0
    }


    private fun classify(
        region: Region2,
        arc: Arc
    ): Type {
        // we want some point near the middle of the arc which is unlikely to coincide with a vertex, because those
        // sometimes sit ambiguously on the edge of the other region
        val result: Ring2.Result = region.test(arc.position(1.0 / E))
        return if (!result.inside) {
            Type.OUTSIDE
        } else if (result.curve == null) {
            Type.INSIDE
        } else {
            if (isTop(arc.first()) == isTop(result.curve))
                Type.SAME_EDGE else Type.DIFF_EDGE
        }
    }

    /**
     * Cuts the rings of a region at the specified vertices, yielding a list of arcs that will serve as the edges of our
     * graph.
     */
    private fun partition(
        region: Region2,
        vertices: Set
    ): List {
        val result: MutableList = mutableListOf()
        for (r in region.rings) {
            val cs: Array = r.curves
            var offset = 0
            while (offset < cs.size) {
                if (vertices.contains(cs[offset].start())) {
                    break
                }
                offset++
            }
            if (offset == cs.size) {
                result.add(Arc(cs.toMutableList()))
            } else {
                var acc = Arc()
                for (i in offset until cs.size) {
                    val c: Curve2 = cs[i]
                    if (vertices.contains(c.start())) {
                        if (acc.size > 0) {
                            result.add(acc)
                        }
                        acc = Arc(mutableListOf(c))
                    } else {
                        acc.add(c)
                    }
                }
                for (i in 0 until offset) {
                    acc.add(cs[i])
                }
                if (acc.size > 0) {
                    result.add(acc)
                }
            }
        }
        return result
    }

    private val SHORTEST_ARC = { x: Arc, y: Arc -> if (x.length() < y.length()) x else y }

    private fun repairGraph(
        graph: DirectedGraph>,
        unused: Iterable
    ): List> {

        // create a graph of all the unused arcs
        val search = DirectedGraph()
        for (arc in unused) {
            search.link(arc.head(), arc.tail(), arc, SHORTEST_ARC)
        }

        // add in the existing arcs as reversed edges, so we can potentially retract them
        for (e in graph.edges()) {
            val arc: Arc = e.value().minByOrNull(
                { obj -> obj.length() })!!

            search.link(arc.tail(), arc.head(), arc, SHORTEST_ARC)
        }

        //search.vertices().forEach(v -> System.out.println(VERTICES.indexOf(v) + " " + search.out(v).stream().map(VERTICES::indexOf).collect(Lists.linearCollector())));
        //graph.vertices().forEach(v -> System.out.println(VERTICES.indexOf(v) + " " + graph.out(v).stream().map(VERTICES::indexOf).collect(Lists.linearCollector())));
        val `in` = graph.vertices()
            .filter { v -> graph.`in`(v).isEmpty() }.toSet()
        val out = graph.vertices()
            .filter { v -> graph.out(v).isEmpty() }.toSet()
        val currIn = (`in` + setOf()).toMutableSet()
        val currOut = (out + setOf()).toMutableSet()

        // attempt to greedily pair our outs and ins
        val result = mutableSetOf>()
        while (currIn.size > 0 && currOut.size > 0) {
            val path = Graphs.shortestPath(search, currOut,
                { value -> `in`.contains(value) },
                { e -> e.value().length() }
            )
            // if our search found a vertex that was previously claimed, we need something better than a greedy search
            if (path == null || !currIn.contains(path.last())) {
                break
            } else {
                currOut.remove(path.first())
                currIn.remove(path.last())
                result.add(edges(path) { from, to -> search.edge(from, to) })
            }
        }
        return if (currIn.size == 0 || currOut.size == 0) {
            result.toList()
        } else Combinatorics.permutations(out.toList())
            .map { vs -> greedyPairing(search, vs, `in`) }
            .minByOrNull { path -> path.sumOf { arcs -> length(arcs) } }
            .orEmpty()

        // Do greedy pairings with every possible vertex ordering, and choose the one that results in the shortest aggregate
        // paths. If `out` is sufficiently large, `permutations` will just return a subset of random shufflings, and it's
        // possible we won't find a single workable solution this time around.
    }

    private fun greedyPairing(
        graph: DirectedGraph,
        out: List,
        `in`: Set
    ): List> {
        val result: MutableList> = mutableListOf()
        val currIn = (`in` + setOf()).toMutableSet()
        for (v in out) {
            // this will only happen if a vertex needs to have multiple edges added/removed, but we'll just get it on the
            // next time around
            if (currIn.size == 0) {
                break
            }
            val path: List = Graphs.shortestPath(graph, listOf(v),
                { value -> currIn.contains(value) },
                { e -> e.value().length() }) ?: (return emptyList())
            currIn.remove(path.last())
            result.add(edges(path) { from, to -> graph.edge(from, to) })
        }
        return result
    }

    private fun  edges(vertices: List, edge: (U, U) -> V): List {
        val result = mutableListOf()
        for (i in 0 until vertices.size - 1) {
            result.add(edge(vertices[i], vertices[(i + 1)]))
        }
        return result
    }

    private fun area(arcs: List): Double {
        return abs(arcs.sumOf { arc -> arc.signedArea() })
    }

    private fun length(arcs: List): Double {
        return abs(arcs.sumOf { arc -> arc.length() })
    }

    private fun ring(arcs: List): Ring2 {
        val acc: MutableList = mutableListOf()
        arcs.forEach { arc ->
            arc.forEach { value -> acc.add(value) }
        }
        return Ring2(acc)
    }

    ///
    fun union(a: Region2, b: Region2): Region2 {
        return operation(a, b,
            Operation.UNION,
            { t -> t == Type.OUTSIDE || t == Type.SAME_EDGE },
             { t: Type -> t == Type.OUTSIDE })
    }

    fun intersection(a: Region2, b: Region2): Region2 {
        return operation(a, b,
            Operation.INTERSECTION,
             { t: Type -> t == Type.INSIDE || t == Type.SAME_EDGE },
             { t: Type -> t == Type.INSIDE })
    }

    fun difference(a: Region2, b: Region2): Region2 {
        return operation(a, b,
            Operation.DIFFERENCE,
             { t: Type -> t == Type.OUTSIDE || t == Type.DIFF_EDGE },
             { t: Type -> t == Type.INSIDE })
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy