First implementation (untested)
This commit is contained in:
100
src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt
Normal file
100
src/main/kotlin/com/jaytux/altgraph/core/BaseGraph.kt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package com.jaytux.altgraph.core
|
||||||
|
|
||||||
|
open class BaseGraph<V, E> : IMutableGraph<V, E> {
|
||||||
|
private val _vertices = mutableMapOf<V, Boolean>()
|
||||||
|
// [from] -> {e: exists v s.t. e = (from, v)}
|
||||||
|
private val _existing = mutableMapOf<V, MutableSet<E>>()
|
||||||
|
// [edge] -> (from, to)
|
||||||
|
private val _edges = mutableMapOf<E, Pair<V, V>>()
|
||||||
|
|
||||||
|
private inner class BaseGraphImmutable : IGraph<V, E> {
|
||||||
|
override fun vertices(): Set<V> = this@BaseGraph.vertices()
|
||||||
|
override fun edges(): Map<E, Pair<V, V>> = this@BaseGraph.edges()
|
||||||
|
override fun roots(): Set<V> = this@BaseGraph.roots()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun vertices(): Set<V> = _vertices.keys
|
||||||
|
override fun edges(): Map<E, Pair<V, V>> = _edges
|
||||||
|
override fun roots(): Set<V> = _vertices.filter { it.value }.keys
|
||||||
|
|
||||||
|
override fun addVertex(vertex: V, isRoot: Boolean): V {
|
||||||
|
if(vertex in _vertices) throw GraphException.vertexAlreadyExists(vertex)
|
||||||
|
_vertices += vertex to isRoot
|
||||||
|
return vertex
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeVertex(vertex: V) {
|
||||||
|
if(vertex !in _vertices)
|
||||||
|
throw GraphException.vertexNotFound(vertex)
|
||||||
|
|
||||||
|
_existing[vertex]?.forEach { edge ->
|
||||||
|
_edges.remove(edge)
|
||||||
|
}
|
||||||
|
_existing.remove(vertex)
|
||||||
|
|
||||||
|
_edges.filter { (_, v) -> v.second == vertex }.forEach { (k, v) ->
|
||||||
|
_edges.remove(k)
|
||||||
|
_existing[v.first]?.remove(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
_vertices.remove(vertex)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun connect(from: V, to: V, edge: E): E {
|
||||||
|
if(from !in _vertices) throw GraphException.vertexNotFound(from)
|
||||||
|
if(to !in _vertices) throw GraphException.vertexNotFound(to)
|
||||||
|
if(edge in _edges) throw GraphException.edgeAlreadyExists(edge)
|
||||||
|
|
||||||
|
if(_existing[from]?.contains(edge) == true)
|
||||||
|
throw GraphException.edgeBetweenAlreadyExists(from, to)
|
||||||
|
|
||||||
|
_edges[edge] = Pair(from, to)
|
||||||
|
_existing.getOrPut(from) { mutableSetOf() } += edge
|
||||||
|
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect(from: V, to: V) {
|
||||||
|
val edge = _existing[from]?.firstOrNull { edge -> _edges[edge]!!.second == to }
|
||||||
|
?: throw GraphException.noEdgeFound(from, to)
|
||||||
|
_edges.remove(edge)
|
||||||
|
_existing[from]?.remove(edge)
|
||||||
|
if (_existing[from]?.isEmpty() == true) { _existing.remove(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeEdge(edge: E) {
|
||||||
|
if(edge !in _edges) throw GraphException.edgeNotFound(edge)
|
||||||
|
|
||||||
|
val (from, to) = _edges.remove(edge)!!
|
||||||
|
_existing[from]?.remove(edge)
|
||||||
|
if (_existing[from]?.isEmpty() == true) { _existing.remove(from) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun immutable(): IGraph<V, E> = BaseGraphImmutable()
|
||||||
|
|
||||||
|
override fun successors(vertex: V): Map<V, E> =
|
||||||
|
_existing[vertex]?.associate { edge -> _edges[edge]!!.second to edge } ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
43
src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt
Normal file
43
src/main/kotlin/com/jaytux/altgraph/core/IGraph.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.jaytux.altgraph.core
|
||||||
|
|
||||||
|
interface IGraph<V, E> {
|
||||||
|
fun vertices(): Set<V>
|
||||||
|
fun edges(): Map<E, Pair<V, V>>
|
||||||
|
fun roots(): Set<V>
|
||||||
|
|
||||||
|
fun successors(vertex: V): Map<V, E> =
|
||||||
|
edges().filter { it.value.first == vertex }
|
||||||
|
.map { (edge, pair) -> pair.second to edge }.toMap()
|
||||||
|
|
||||||
|
fun predecessors(vertex: V): Map<V, E> =
|
||||||
|
edges().filter { it.value.second == vertex }
|
||||||
|
.map { (edge, pair) -> pair.first to edge }.toMap()
|
||||||
|
|
||||||
|
fun xToY(x: V, y: V): E? = edges().entries.firstOrNull { it.value == x to y }?.key
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMutableGraph<V, E> : IGraph<V, E> {
|
||||||
|
fun addVertex(vertex: V, isRoot: Boolean = false): V
|
||||||
|
fun removeVertex(vertex: V)
|
||||||
|
fun connect(from: V, to: V, edge: E): E
|
||||||
|
fun disconnect(from: V, to: V)
|
||||||
|
fun removeEdge(edge: E)
|
||||||
|
}
|
||||||
|
|
||||||
|
class GraphException(message: String) : RuntimeException(message) {
|
||||||
|
companion object {
|
||||||
|
fun <V> vertexAlreadyExists(vertex: V) =
|
||||||
|
GraphException("Vertex '$vertex' already exists in this graph.")
|
||||||
|
fun <E> edgeAlreadyExists(edge: E) =
|
||||||
|
GraphException("Edge '$edge' already exists in this graph.")
|
||||||
|
fun <V> edgeBetweenAlreadyExists(from: V, to: V) =
|
||||||
|
GraphException("Edge from '$from' to '$to' already exists in this graph.")
|
||||||
|
|
||||||
|
fun <V> vertexNotFound(vertex: V) =
|
||||||
|
GraphException("Vertex '$vertex' not found in this graph.")
|
||||||
|
fun <V> noEdgeFound(from: V, to: V) =
|
||||||
|
GraphException("No edge found from '$from' to '$to' in this graph.")
|
||||||
|
fun <E> edgeNotFound(edge: E) =
|
||||||
|
GraphException("Edge '$edge' not found in this graph.")
|
||||||
|
}
|
||||||
|
}
|
13
src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt
Normal file
13
src/main/kotlin/com/jaytux/altgraph/layout/Geometry.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
interface ISize<S : ISize<S>> {
|
||||||
|
fun width(): Float
|
||||||
|
fun height(): Float
|
||||||
|
fun copy(width: Float, height: Float): S
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPoint<P : IPoint<P>> {
|
||||||
|
fun x(): Float
|
||||||
|
fun y(): Float
|
||||||
|
fun copy(x: Float, y: Float): P
|
||||||
|
}
|
13
src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt
Normal file
13
src/main/kotlin/com/jaytux/altgraph/layout/ILayout.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
|
||||||
|
interface ILayout<V, E, P : IPoint<P>, S : ISize<S>> {
|
||||||
|
fun graph(): IGraph<V, E>
|
||||||
|
fun setGraph(graph: IGraph<V, E>)
|
||||||
|
fun compute()
|
||||||
|
fun location(vertex: V): P
|
||||||
|
fun freezeAt(vertex: V, point: P)
|
||||||
|
fun unfreeze(vertex: V)
|
||||||
|
fun boundingBox(): S
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
data class MutablePair<T1, T2>(var x: T1, var y: T2)
|
282
src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt
Normal file
282
src/main/kotlin/com/jaytux/altgraph/layout/PseudoForestLayout.kt
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.core.GraphException
|
||||||
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
import com.jaytux.altgraph.layout.SumType.Companion.sum1
|
||||||
|
import com.jaytux.altgraph.layout.SumType.Companion.sum2
|
||||||
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
||||||
|
graph: IGraph<V, E>,
|
||||||
|
var horizontalMargin: Float,
|
||||||
|
var disjoinXMargin: Float,
|
||||||
|
var interLayer: Float,
|
||||||
|
val pointZero: P, val sizeZero: S,
|
||||||
|
vertexSize: (V) -> VertexSize<P, S>
|
||||||
|
) : ILayout<V, E, P, S> {
|
||||||
|
data class VertexSize<P : IPoint<P>, S : ISize<S>>(val vertex: S, val labelOffset: P, val labelSize: S) {
|
||||||
|
fun fullSize(): S { // TODO: check the math here
|
||||||
|
val minX = 0 + labelOffset.x()
|
||||||
|
val minY = 0 + labelOffset.y()
|
||||||
|
val maxX = vertex.width() + labelOffset.x() + labelSize.width()
|
||||||
|
val maxY = vertex.height() + labelOffset.y() + labelSize.height()
|
||||||
|
return vertex.copy(
|
||||||
|
width = maxX - minX,
|
||||||
|
height = maxY - minY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vCenterInBox(): P { // TODO: check the math here
|
||||||
|
return labelOffset.copy(
|
||||||
|
x = labelOffset.x() + vertex.width() / 2,
|
||||||
|
y = labelOffset.y() + vertex.height() / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var _graph: IGraph<V, E> = graph
|
||||||
|
private val _positions = mutableMapOf<V, P>()
|
||||||
|
private var _vertexSize: (V) -> VertexSize<P, S> = vertexSize
|
||||||
|
private var _boundingBox: S? = null
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
|
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
|
private inline fun <R> locked(block: () -> R): R {
|
||||||
|
while (!_lock.compareAndSet(expectedValue = false, newValue = true)) {
|
||||||
|
// Do a big ol' spinny-spin wait
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val x = block()
|
||||||
|
_lock.store(false) // unlock after operation
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
_lock.store(false) // we can safely unlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun graph(): IGraph<V, E> = _graph
|
||||||
|
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
|
||||||
|
fun setVertexSize(vertexSize: (V) -> VertexSize<P, S>) { locked { _vertexSize = vertexSize } }
|
||||||
|
|
||||||
|
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
||||||
|
private data class Connector<V>(
|
||||||
|
var from: LayeredVertex<V>,
|
||||||
|
var to: LayeredVertex<V>
|
||||||
|
)
|
||||||
|
private data class LayeredVertex<V>(
|
||||||
|
val x: SumType<V, Connector<V>>
|
||||||
|
) {
|
||||||
|
constructor(x: V): this(x.sum1())
|
||||||
|
constructor(x: Connector<V>): this(x.sum2())
|
||||||
|
|
||||||
|
fun same(other: LayeredVertex<V>) = x.fold({ it1 ->
|
||||||
|
other.x.fold({ it2 -> it1 == it2 }) { false }
|
||||||
|
}) { it1 ->
|
||||||
|
other.x.fold({ false }) { it2 -> it1.from == it2.from && it1.to == it2.to }
|
||||||
|
}
|
||||||
|
|
||||||
|
// true is this is a direct parent of the other vertex/connector
|
||||||
|
fun directParentOf(other: LayeredVertex<V>, graph: IGraph<V, *>): Boolean =
|
||||||
|
x.fold({ xx ->
|
||||||
|
// x is a vertex
|
||||||
|
other.x.fold({
|
||||||
|
// other is a vertex
|
||||||
|
graph.xToY(xx, it) != null
|
||||||
|
}) {
|
||||||
|
// other is a connector
|
||||||
|
it.from.same(this)
|
||||||
|
}
|
||||||
|
}) { xx ->
|
||||||
|
// x is a connector -> we need to connect to other
|
||||||
|
xx.to.same(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private data class PreConnector<V>(
|
||||||
|
var x: SumType<LayeredVertex<V>, PreConnector<V>>?,
|
||||||
|
var y: SumType<LayeredVertex<V>, PreConnector<V>>?
|
||||||
|
)
|
||||||
|
private fun realVertex(v: V) = LayeredVertex(v.sum1())
|
||||||
|
private fun buildChain(from: V, to: V, layerF: Int, layerT: Int): List<LayeredVertex<V>> {
|
||||||
|
val chain = mutableListOf(LayeredVertex(Connector(LayeredVertex(from), LayeredVertex(to))))
|
||||||
|
while(chain.size < layerT - layerF - 1) {
|
||||||
|
val last = chain.last() // last is always Connector<V>, and last.to is always V (== to)
|
||||||
|
val lastX = (last.x as SumType.SumT2<Connector<V>>).value
|
||||||
|
val next = LayeredVertex(Connector(last, lastX.to))
|
||||||
|
lastX.to = next // reconnect
|
||||||
|
chain += next
|
||||||
|
}
|
||||||
|
return chain
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compute() {
|
||||||
|
locked {
|
||||||
|
_positions.clear()
|
||||||
|
|
||||||
|
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
||||||
|
val roots = _graph.roots()
|
||||||
|
if (roots.isEmpty()) { // Only reachable nodes matter.
|
||||||
|
_boundingBox = sizeZero
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||||
|
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
|
||||||
|
queue.addAll(roots.map { it to emptySet() })
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
val (vertex, onPath) = queue.removeFirst()
|
||||||
|
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||||
|
|
||||||
|
val succLayer = layer + 1
|
||||||
|
_graph.successors(vertex).forEach { (succ, _) ->
|
||||||
|
dep += succ
|
||||||
|
|
||||||
|
if (succ in onPath) return@forEach
|
||||||
|
layers[succ]?.let { (l, sDep) ->
|
||||||
|
val delta = succLayer - l
|
||||||
|
if (delta > 0) {
|
||||||
|
layers[succ] = succLayer to (sDep)
|
||||||
|
sDep.forEach { dep ->
|
||||||
|
layers[dep] = layers[dep]?.let { (dl, dd) -> dl + delta to dd }
|
||||||
|
?: (succLayer to mutableSetOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue.addLast(succ to onPath + vertex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache node sizes
|
||||||
|
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
|
||||||
|
|
||||||
|
// Compute layer y positions (and thus the bounding box height).
|
||||||
|
val layerCount = layers.maxOf { it.value.first }
|
||||||
|
val layerHeights = MutableList(layerCount) { 0.0f }
|
||||||
|
|
||||||
|
var minOffset = Float.POSITIVE_INFINITY
|
||||||
|
var maxOffset = Float.NEGATIVE_INFINITY
|
||||||
|
layers.forEach { (vertex, pair) ->
|
||||||
|
val (layer, _) = pair
|
||||||
|
val size = vertexSizes[vertex]!!
|
||||||
|
layerHeights[layer] = max(layerHeights[layer], size.first.height())
|
||||||
|
if(layer == 0) {
|
||||||
|
// Take into account vertex bounding box offset
|
||||||
|
val delta = size.second.y()
|
||||||
|
if(delta < minOffset) minOffset = delta
|
||||||
|
if(delta > maxOffset) maxOffset = delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val offset = maxOffset - minOffset
|
||||||
|
var totalHeight = 0.0f
|
||||||
|
val layerY = MutableList(layerCount) { idx ->
|
||||||
|
val y = totalHeight + layerHeights[idx] / 2 + offset
|
||||||
|
totalHeight += layerHeights[idx] + interLayer
|
||||||
|
y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute disjoint graphs
|
||||||
|
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||||
|
val reachable = layers[root]?.second?.toMutableSet() ?: return@fold acc
|
||||||
|
|
||||||
|
val dedup = acc.mapNotNull { other ->
|
||||||
|
val inter = reachable intersect other
|
||||||
|
if(inter.isEmpty()) other // fully disjoint -> keep
|
||||||
|
else { // not disjoint -> merge and remove
|
||||||
|
reachable.addAll(other)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
dedup.add(reachable)
|
||||||
|
dedup
|
||||||
|
}
|
||||||
|
var currentXZero = 0.0f
|
||||||
|
disjoint.forEach { sub ->
|
||||||
|
// Put each vertex in a list by layer
|
||||||
|
val layered = List(layerCount) { layer ->
|
||||||
|
sub.mapNotNull {
|
||||||
|
if(layers[it]?.first == layer) realVertex(it)
|
||||||
|
else null
|
||||||
|
}.toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break up multi-layer edges with dummy nodes
|
||||||
|
layered.forEachIndexed { idx, list ->
|
||||||
|
list.forEach { v ->
|
||||||
|
v.x.fold({ node ->
|
||||||
|
val successors = _graph.successors(node)
|
||||||
|
successors.forEach { (other, _) ->
|
||||||
|
val otherLayer = layers[other]?.first ?: return@forEach
|
||||||
|
if(otherLayer > idx + 1) {
|
||||||
|
val chain = buildChain(node, other, idx, otherLayer)
|
||||||
|
chain.forEachIndexed { offset, dummy ->
|
||||||
|
layered[idx + offset + 1] += dummy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {} // do nothing on dummy nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val layerWidths = MutableList(layerCount) { -horizontalMargin } // avoid double adding margin on 1st node
|
||||||
|
// Layer-by-layer, assign x slots (not yet positions)
|
||||||
|
for(i in 1 until layered.size) {
|
||||||
|
// Barycenter heuristic: average of parents' slots
|
||||||
|
val heuristic = { v: LayeredVertex<V> ->
|
||||||
|
val parents = layered[i - 1].mapIndexedNotNull { idx, p -> if(p.directParentOf(v, _graph)) idx.toFloat() else null }
|
||||||
|
parents.sum() / parents.size
|
||||||
|
}
|
||||||
|
layered[i].sortBy { heuristic(it) }
|
||||||
|
|
||||||
|
layered[i].forEach { v ->
|
||||||
|
v.x.fold({ node ->
|
||||||
|
val w = vertexSizes[node]!!.first.width()
|
||||||
|
layerWidths[i] += w + horizontalMargin
|
||||||
|
}) { /* do nothing */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: try to do some swaps in order to minimize #crossings?
|
||||||
|
}
|
||||||
|
val maxWidth = layerWidths.max()
|
||||||
|
|
||||||
|
// TODO: do some reorderings to minimize #crossings?
|
||||||
|
|
||||||
|
// Assign x positions
|
||||||
|
layered.forEachIndexed { idx, layer ->
|
||||||
|
var currentX = currentXZero + (maxWidth - layerWidths[idx]) / 2
|
||||||
|
layer.forEach { v ->
|
||||||
|
v.x.fold({ node ->
|
||||||
|
val offset = vertexSizes[node]!!.second
|
||||||
|
_positions[node] = pointZero.copy(x = currentX + offset.x(), y = layerY[idx] + offset.y())
|
||||||
|
currentX += vertexSizes[node]!!.first.width() + horizontalMargin
|
||||||
|
}) { /* do nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update X-zero for the next disjoint graph
|
||||||
|
currentXZero += maxWidth + disjoinXMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the bounding box
|
||||||
|
// min x and y are 0
|
||||||
|
_boundingBox = sizeZero.copy(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun location(vertex: V): P {
|
||||||
|
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||||
|
return _positions[vertex] ?: run {
|
||||||
|
compute()
|
||||||
|
_positions[vertex]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun freezeAt(vertex: V, point: P) = throw UnsupportedOperationException("PseudoForestLayout does not allow freezing vertices.")
|
||||||
|
override fun unfreeze(vertex: V) { /* no-op: cannot freeze vertices */ }
|
||||||
|
|
||||||
|
override fun boundingBox(): S = _boundingBox ?: run { compute(); _boundingBox!! }
|
||||||
|
}
|
25
src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt
Normal file
25
src/main/kotlin/com/jaytux/altgraph/layout/SumType.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
|
sealed class SumType<out T1, out T2> {
|
||||||
|
class SumT1<out T1>(val value: T1) : SumType<T1, Nothing>() {
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
other is SumT1<*> && value == other.value
|
||||||
|
override fun hashCode(): Int = value.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SumT2<out T2>(val value: T2) : SumType<Nothing, T2>() {
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
other is SumT2<*> && value == other.value
|
||||||
|
override fun hashCode(): Int = value.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <R> fold(onT1: (T1) -> R, onT2: (T2) -> R): R = when(this) {
|
||||||
|
is SumT1 -> onT1(value)
|
||||||
|
is SumT2 -> onT2(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> T.sum1(): SumType<T, Nothing> = SumT1(this)
|
||||||
|
fun <T> T.sum2(): SumType<Nothing, T> = SumT2(this)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
|
import com.jaytux.altgraph.layout.IPoint
|
||||||
|
import com.jaytux.altgraph.layout.ISize
|
||||||
|
import java.awt.Dimension
|
||||||
|
import java.awt.Point
|
||||||
|
|
||||||
|
class GSize(val size: Dimension) : ISize<GSize> {
|
||||||
|
override fun width(): Float = size.width.toFloat()
|
||||||
|
override fun height(): Float = size.height.toFloat()
|
||||||
|
override fun copy(width: Float, height: Float): GSize =
|
||||||
|
GSize(Dimension(width.toInt(), height.toInt()))
|
||||||
|
|
||||||
|
override fun hashCode(): Int = size.hashCode()
|
||||||
|
override fun equals(other: Any?): Boolean = other is GSize && size == other.size
|
||||||
|
override fun toString(): String = "$size"
|
||||||
|
}
|
||||||
|
|
||||||
|
class GPoint(val point: Point) : IPoint<GPoint> {
|
||||||
|
override fun x(): Float = point.x.toFloat()
|
||||||
|
override fun y(): Float = point.y.toFloat()
|
||||||
|
override fun copy(x: Float, y: Float): GPoint =
|
||||||
|
GPoint(Point(x.toInt(), y.toInt()))
|
||||||
|
|
||||||
|
override fun hashCode(): Int = point.hashCode()
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
other is GPoint && point == other.point
|
||||||
|
override fun toString(): String = "$point"
|
||||||
|
}
|
Reference in New Issue
Block a user