Compare commits

...

4 Commits

Author SHA1 Message Date
b00dc96f5b Improved layouting (minimize #crossings) 2025-09-09 19:07:31 +02:00
6f0f5d05b6 Prepare alpha-release 2025-09-05 11:21:41 +02:00
9f78c3e44a Working on minimizing crossings 2025-09-05 10:32:23 +02:00
2d36d60020 Fixed layouting, added another example 2025-09-02 18:08:46 +02:00
6 changed files with 286 additions and 67 deletions

View File

@ -1,15 +1,19 @@
plugins { plugins {
kotlin("jvm") version "2.2.0" kotlin("jvm") version "2.2.0"
id("org.jetbrains.dokka") version "2.0.0"
`java-library`
`maven-publish`
} }
group = "com.jaytux.altgraph" group = "com.github.jaytux"
version = "1.0-SNAPSHOT" version = "1.0-alpha"
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
api(kotlin("stdlib"))
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
} }
@ -18,4 +22,23 @@ tasks.test {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(21)
}
tasks.register<Jar>("dokkaJavadocJar") {
dependsOn(tasks.dokkaJavadoc)
from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
archiveClassifier.set("javadoc")
}
java {
withSourcesJar()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
artifact(tasks.named("dokkaJavadocJar"))
}
}
} }

View File

@ -0,0 +1,65 @@
package com.jaytux.altgraph.examples
import com.jaytux.altgraph.core.BaseGraph
import com.jaytux.altgraph.core.IGraph
import com.jaytux.altgraph.layout.PseudoForestLayout
import com.jaytux.altgraph.swing.DefaultVertexComponent
import com.jaytux.altgraph.swing.GraphPane
import com.jaytux.altgraph.swing.QuadraticEdge
import java.awt.Color
import javax.swing.JFrame
import javax.swing.SwingUtilities
data class Edge(val id: Int, val isWrite: Boolean)
fun getRegGraph(): IGraph<String, Edge> {
val graph = BaseGraph<String, Edge>()
val allowed = setOf(633, 631, 632, 634, 638, 635, 636)
val roots = listOf(647, 645, 643, 530, 519, 512, 513, 522, 523, 631, 632, 633, 655).sorted().filter { it in allowed }
val others = listOf(533, 550, 547, 552, 637, 638, 635, 634, 548, 659, 657, 636, 639, 557, 642, 640, 644, 646, 648, 650, 652, 653, 654, 656, 658).sorted().filter { it in allowed }
val regs = roots.associateWith { graph.addVertex("%reg$it", true) } + others.associateWith { graph.addVertex("%reg$it", false) }
var count = 0
val normal = listOf(
647 to 648, 645 to 646, 519 to 533, 519 to 557, 519 to 639, 512 to 550, 512 to 547, 513 to 552, 522 to 659, 522 to 657, 523 to 637, 631 to 638, 631 to 635,
632 to 634, 633 to 634, 633 to 636, 547 to 548, 637 to 659, 637 to 557, 637 to 639, 637 to 657, 635 to 636, 639 to 642, 639 to 640, 639 to 644, 646 to 648,
648 to 650, 639 to 648, 639 to 650, 639 to 653, 650 to 652, 650 to 652, 650 to 656, 650 to 654, 652 to 653, 654 to 656, 655 to 656, 650 to 658, 557 to 642,
557 to 654, 644 to 646, 657 to 658
).filter{ (f,s) -> f in allowed && s in allowed }.map { it to false }
val writes = listOf(
656 to 643, 550 to 519, 548 to 513, 552 to 522, 658 to 522, 637 to 522, 636 to 631, 634 to 631
).filter{ (f,s) -> f in allowed && s in allowed }.map { it to true }
(normal + writes).forEach { (p, isWrite) ->
val (from, to) = p
graph.connect(regs[from]!!, regs[to]!!, Edge(count++, isWrite))
}
return graph
}
fun getRegPane(graph: IGraph<String, Edge>): GraphPane<String, Edge> = GraphPane(graph) { pane, graph ->
PseudoForestLayout(graph, 10.0f, 20.0f, 10.0f, { it, p -> it.isWrite && p == PseudoForestLayout.LayoutPhase.LAYERING }) { v ->
(pane.getComponentFor(v) as DefaultVertexComponent).vertexSize()
}
}.also { pane ->
val edge = QuadraticEdge<Edge>(
delta = 0.0f,
color = { if(it.isWrite) Color.ORANGE.darker() else Color.BLACK }
)
pane.setEdgeRenderer(edge)
}
fun main() {
val graph = getRegGraph()
val pane = getRegPane(graph)
SwingUtilities.invokeLater {
val frame = JFrame("Simple Register Dependency Graph")
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
frame.setSize(800, 600)
frame.add(pane)
frame.pack()
frame.setLocationRelativeTo(null)
frame.isVisible = true
}
}

View File

@ -14,6 +14,15 @@ import kotlin.math.max
* This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly. * This algorithm arranges the graph in layers, attempting to minimize edge crossings and distribute nodes evenly.
* It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout. * It treats the graph as a collection of disjoint acyclic graphs, disregarding back-edges during layout.
* *
* You can improve the layout by increasing the iteration count via [setIterationCount], at the cost of longer
* computation time (default: 3).
*
* Additionally, you can ignore certain edges during layout by providing a function to [ignoreInLayout]; during each
* of the phases where edges can be ignored ([LayoutPhase.LAYERING] (deciding layers for each vertex),
* [LayoutPhase.DISJOINTS] (deciding which sub-graphs to consider disjoint), and [LayoutPhase.SLOT_ASSIGNMENT] (deciding
* the order of vertices in each layer)), the function will be called for each edge (once), and if it returns true,
* the edge will be ignored for that phase. By default, no edges are ignored.
*
* This layout algorithm does not support freezing vertices; calling [freezeAt] will throw * This layout algorithm does not support freezing vertices; calling [freezeAt] will throw
* [UnsupportedOperationException], and [unfreeze] is a no-op. * [UnsupportedOperationException], and [unfreeze] is a no-op.
* Additionally, [setGraph] and [setVertexSize] do not invalidate the cache. * Additionally, [setGraph] and [setVertexSize] do not invalidate the cache.
@ -33,6 +42,7 @@ import kotlin.math.max
* @property horizontalMargin the horizontal margin between vertices in the same layer * @property horizontalMargin the horizontal margin between vertices in the same layer
* @property disjoinXMargin the horizontal margin between disjoint subgraphs * @property disjoinXMargin the horizontal margin between disjoint subgraphs
* @property interLayer the vertical margin between layers * @property interLayer the vertical margin between layers
* @property ignoreInLayout a function that returns true if an edge should be ignored during layout (but are not inherently back-edges)
* @property vertexSize a function that returns the size of a vertex, including its label offset * @property vertexSize a function that returns the size of a vertex, including its label offset
* *
* @see ILayout * @see ILayout
@ -42,9 +52,22 @@ class PseudoForestLayout<V, E>(
var horizontalMargin: Float, var horizontalMargin: Float,
var disjoinXMargin: Float, var disjoinXMargin: Float,
var interLayer: Float, var interLayer: Float,
val ignoreInLayout: (E, LayoutPhase) -> Boolean = { _, _ -> false },
vertexSize: (V) -> VertexSize vertexSize: (V) -> VertexSize
) : ILayout<V, E> ) : ILayout<V, E>
{ {
/**
* An enum representing the different phases of the layout process where edges can be ignored.
* - [LAYERING]: during the layering phase, where back-edges are ignored to determine layers.
* - [DISJOINTS]: during the disjoint graph computation phase, where edges connecting disjoint subgraphs can be ignored.
* - [SLOT_ASSIGNMENT]: during the slot assignment phase, where certain edges may be ignored to optimize layout.
*/
enum class LayoutPhase {
LAYERING,
DISJOINTS,
SLOT_ASSIGNMENT
}
/** /**
* A class representing data on the size of a vertex, including its label offset and size. * A class representing data on the size of a vertex, including its label offset and size.
* *
@ -85,6 +108,7 @@ class PseudoForestLayout<V, E>(
private val _positions = mutableMapOf<V, GPoint>() private val _positions = mutableMapOf<V, GPoint>()
private var _vertexSize: (V) -> VertexSize = vertexSize private var _vertexSize: (V) -> VertexSize = vertexSize
private var _boundingBox: GSize? = null private var _boundingBox: GSize? = null
private var _repeat: Int = 3
@OptIn(ExperimentalAtomicApi::class) @OptIn(ExperimentalAtomicApi::class)
private var _lock: AtomicBoolean = AtomicBoolean(false) private var _lock: AtomicBoolean = AtomicBoolean(false)
@ -110,6 +134,20 @@ class PseudoForestLayout<V, E>(
override fun graph(): IGraph<V, E> = _graph override fun graph(): IGraph<V, E> = _graph
override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } } override fun setGraph(graph: IGraph<V, E>) { locked { _graph = graph } }
/**
* Gets the number of iterations the layout algorithm will perform to optimize the layout.
*
* @return the number of iterations
*/
fun getIterationCount(): Int = _repeat
/**
* Sets the number of iterations the layout algorithm will perform to optimize the layout.
* More iterations may yield a better layout, but will take longer to compute.
*
* @param repeat the number of iterations
*/
fun setIterationCount(repeat: Int) { locked { _repeat = repeat } }
/** /**
* Sets the vertex measuring function. * Sets the vertex measuring function.
* *
@ -117,56 +155,93 @@ class PseudoForestLayout<V, E>(
*/ */
fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } } fun setVertexSize(vertexSize: (V) -> VertexSize) { locked { _vertexSize = vertexSize } }
// Either a vertex, or a dummy node to break up multi-layer-spanning edges private class Conn<V> private constructor(var from: Vert<V>?, var to: Vert<V>?, private val _id: Int) {
private data class Connector<V>( constructor(from: Vert<V>?, to: Vert<V>?) : this(from, to, _nextId++) {}
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 -> override fun equals(other: Any?): Boolean = other is Conn<*> && other._id == _id
override fun hashCode(): Int = _id.hashCode()
override fun toString(): String = "C[${from?.x?.fold({ it.toString() }) { "C" } ?: "null"}->${to?.x?.fold({ it.toString() }) { "C" } ?: "null"}@$_id]"
companion object {
private var _nextId = 0
}
}
private class Vert<V>(val x: SumType<V, Conn<V>>) {
constructor(x: V): this(x.sum1())
constructor(x: Conn<V>): this(x.sum2())
override fun toString(): String = x.fold({ it.toString() }) { it.toString() }
fun same(other: Vert<V>) = x.fold({ it1 ->
other.x.fold({ it2 -> it1 == it2 }) { false } other.x.fold({ it2 -> it1 == it2 }) { false }
}) { it1 -> }) { it1 ->
other.x.fold({ false }) { it2 -> it1.from == it2.from && it1.to == it2.to } other.x.fold({ false }) { it2 -> it1 == it2 }
} }
// true is this is a direct parent of the other vertex/connector fun directParentOf(other: Vert<V>, graph: IGraph<V, *>): Boolean = x.fold({ it1 ->
fun directParentOf(other: LayeredVertex<V>, graph: IGraph<V, *>): Boolean = other.x.fold({ it2 -> graph.xToY(it1, it2) != null }) { it2 ->
x.fold({ xx -> it2.from?.x == it1
// 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)
} }
}) { it1 ->
it1.to!!.same(other)
}
} }
private data class PreConnector<V>(
var x: SumType<LayeredVertex<V>, PreConnector<V>>?, private fun buildChain(from: V, to: V, layerF: Int, layerT: Int): List<Vert<V>> {
var y: SumType<LayeredVertex<V>, PreConnector<V>>? val first = Vert(from)
) val last = Vert(to)
private fun realVertex(v: V) = LayeredVertex(v.sum1()) val chain = List(layerT - layerF - 1) { Vert(Conn<V>(null, null)) }
private fun buildChain(from: V, to: V, layerF: Int, layerT: Int): List<LayeredVertex<V>> {
val chain = mutableListOf(LayeredVertex(Connector(LayeredVertex(from), LayeredVertex(to)))) chain.forEachIndexed { i, v ->
while(chain.size < layerT - layerF - 1) { if(i == 0) (v.x.asT2).from = first
val last = chain.last() // last is always Connector<V>, and last.to is always V (== to) else (v.x.asT2).from = chain[i - 1]
val lastX = (last.x as SumType.SumT2<Connector<V>>).value
val next = LayeredVertex(Connector(last, lastX.to)) if(i == chain.size - 1) (v.x.asT2).to = last
lastX.to = next // reconnect else (v.x.asT2).to = chain[i + 1]
chain += next
} }
return chain return chain
} }
private fun reachableFrom(start: V, phase: LayoutPhase): Set<V> {
val seen = mutableSetOf<V>()
val queue = ArrayDeque<V>()
queue.add(start)
while(!queue.isEmpty()) {
val v = queue.removeFirst()
if(seen.add(v)) {
_graph.successors(v).forEach { (succ, edge) ->
if(ignoreInLayout(edge, phase)) return@forEach
if(succ !in seen) queue.addLast(succ)
}
}
}
return seen
}
private fun computeCrossings(lTop: List<Vert<V>>, lBot: List<Vert<V>>, edges: List<Pair<Vert<V>, Vert<V>>>): Int {
var count = 0
val conns = edges.map { (top, bot) -> lTop.indexOf(top) to lBot.indexOf(bot) }.sortedWith { (t1, b1), (t2, b2) ->
if (t1 != t2) t1 - t2
else b1 - b2
}
conns.forEachIndexed { i, conn ->
for(j in i + 1 until conns.size) {
val other = conns[j]
if(conn.first == other.first || conn.second == other.second) continue // shared vertex -> cannot cross
val topFirst = conn.first < other.first
val botFirst = conn.second < other.second
if(topFirst != botFirst) count++
}
}
return count
}
override fun compute() { override fun compute() {
println("Acquiring lock") println("Acquiring lock")
locked { locked {
@ -182,20 +257,20 @@ class PseudoForestLayout<V, E>(
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>() val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2) val queue = ArrayDeque<Pair<V, Set<V>>>(roots.size * 2)
queue.addAll(roots.map { it to emptySet() }) queue.addAll(roots.map { it to emptySet() })
println(" - Computing layers from roots: $roots")
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
val (vertex, onPath) = queue.removeFirst() val (vertex, onPath) = queue.removeFirst()
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() } val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
val succLayer = layer + 1 val succLayer = layer + 1
_graph.successors(vertex).forEach { (succ, _) -> _graph.successors(vertex).forEach { (succ, edge) ->
if(ignoreInLayout(edge, LayoutPhase.LAYERING)) {
return@forEach
}
if (succ in onPath) return@forEach if (succ in onPath) return@forEach
dep += succ dep += succ
layers[succ]?.let { (l, sDep) -> layers[succ]?.let { (l, sDep) ->
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
dep += sDep dep += sDep
val delta = succLayer - l val delta = succLayer - l
@ -209,15 +284,12 @@ class PseudoForestLayout<V, E>(
} ?: run { } ?: run {
layers[succ] = succLayer to mutableSetOf() layers[succ] = succLayer to mutableSetOf()
} }
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
queue.addLast(succ to onPath + vertex) queue.addLast(succ to onPath + vertex)
} }
// ensure dependents are always up to date // ensure dependents are always up to date
layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) } layers.values.filter { it.second.contains(vertex) }.forEach { (_, d) -> d.addAll(dep) }
} }
println(" - Assigned layers:")
layers.forEach { (v, p) -> println(" - Vertex $v: layer ${p.first}, dependents: ${p.second}") }
// Cache node sizes // Cache node sizes
val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } } val vertexSizes = layers.mapValues { (v, _) -> _vertexSize(v).let { it.fullSize() to it.vCenterInBox() } }
@ -225,7 +297,6 @@ class PseudoForestLayout<V, E>(
// Compute layer y positions (and thus the bounding box height). // Compute layer y positions (and thus the bounding box height).
val layerCount = layers.maxOf { it.value.first } + 1 val layerCount = layers.maxOf { it.value.first } + 1
val layerHeights = MutableList(layerCount) { 0.0f } val layerHeights = MutableList(layerCount) { 0.0f }
println(" - Have $layerCount layers")
var minOffset = Float.POSITIVE_INFINITY var minOffset = Float.POSITIVE_INFINITY
var maxOffset = Float.NEGATIVE_INFINITY var maxOffset = Float.NEGATIVE_INFINITY
@ -248,18 +319,9 @@ class PseudoForestLayout<V, E>(
y y
} }
println(" - Layer measurements: (height, y) = ${(layerHeights zip layerY)}")
println(" - Layers with nodes:")
for(i in 0 until layerCount) {
val verts = layers.filter { it.value.first == i }.keys
println(" - Layer $i: $verts")
}
// Compute disjoint graphs // Compute disjoint graphs
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root -> val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
val reachable = layers[root]?.second?.toMutableSet() ?: return@fold acc val reachable = reachableFrom(root, LayoutPhase.DISJOINTS).toMutableSet()
reachable += root
val dedup = acc.mapNotNull { other -> val dedup = acc.mapNotNull { other ->
val inter = reachable intersect other val inter = reachable intersect other
if(inter.isEmpty()) other // fully disjoint -> keep if(inter.isEmpty()) other // fully disjoint -> keep
@ -274,18 +336,14 @@ class PseudoForestLayout<V, E>(
} }
var currentXZero = 0.0f var currentXZero = 0.0f
disjoint.forEach { sub -> disjoint.forEach { sub ->
println(" - Layouting disjoint subgraph: $sub")
// Put each vertex in a list by layer // Put each vertex in a list by layer
val layered = List(layerCount) { layer -> val layered = List(layerCount) { layer ->
sub.mapNotNull { sub.mapNotNull {
if(layers[it]?.first == layer) realVertex(it) if(layers[it]?.first == layer) Vert(it)
else null else null
}.toMutableList() }.toMutableList()
} }
println(" - Initial layered vertices:")
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
// Break up multi-layer edges with dummy nodes // Break up multi-layer edges with dummy nodes
layered.forEachIndexed { idx, list -> layered.forEachIndexed { idx, list ->
list.forEach { v -> list.forEach { v ->
@ -299,6 +357,12 @@ class PseudoForestLayout<V, E>(
layered[idx + offset + 1] += dummy layered[idx + offset + 1] += dummy
} }
} }
else if(otherLayer < idx - 1) {
val chain = buildChain(other, node, otherLayer, idx)
chain.forEachIndexed { offset, dummy ->
layered[otherLayer + offset + 1] += dummy
}
}
} }
}) {} // do nothing on dummy nodes }) {} // do nothing on dummy nodes
} }
@ -317,7 +381,7 @@ class PseudoForestLayout<V, E>(
// Layer-by-layer, assign x slots (not yet positions) // Layer-by-layer, assign x slots (not yet positions)
for(i in 1 until layered.size) { for(i in 1 until layered.size) {
// Barycenter heuristic: average of parents' slots // Barycenter heuristic: average of parents' slots
val heuristic = { v: LayeredVertex<V> -> val heuristic = { v: Vert<V> ->
val parents = layered[i - 1].mapIndexedNotNull { idx, p -> if(p.directParentOf(v, _graph)) idx.toFloat() else null } val parents = layered[i - 1].mapIndexedNotNull { idx, p -> if(p.directParentOf(v, _graph)) idx.toFloat() else null }
parents.sum() / parents.size parents.sum() / parents.size
} }
@ -335,6 +399,48 @@ class PseudoForestLayout<V, E>(
val maxWidth = layerWidths.max() val maxWidth = layerWidths.max()
// TODO: do some reorderings to minimize #crossings? // TODO: do some reorderings to minimize #crossings?
println(" - Optimizing slot assignments")
layered.forEachIndexed { idx, list ->
println(" - Layer $idx: $list")
}
val edges = mutableListOf<MutableList<Pair<Vert<V>, Vert<V>>>>()
for(i in 1 until layered.size) {
edges.add(mutableListOf())
val current = edges[i - 1]
layered[i].forEach { vBot ->
val forVBot = layered[i - 1].filter {
it.directParentOf(vBot, _graph) || vBot.directParentOf(it, _graph)
}.map { it to vBot }
current.addAll(forVBot)
}
}
repeat(_repeat) {
val optP0 = layered[0].permutations().map { perm ->
val crossings = computeCrossings(perm, layered[1], edges[0])
println(" - Layer 0 permutation $perm (to ${layered[1]} has $crossings crossings")
perm to crossings
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
if (accCost > cost) cost.toFloat() to perm
else accCost to accSeq
}
layered[0].clear()
layered[0].addAll(optP0.second.toMutableList())
for (i in 1 until layered.size) {
val optPi = layered[i].permutations().map { perm ->
val crossings = computeCrossings(layered[i - 1], perm, edges[i - 1])
println(" - Layer $i permutation $perm (from ${layered[i - 1]} has $crossings crossings")
perm to crossings
}.fold(Float.POSITIVE_INFINITY to listOf<Vert<V>>()) { (accCost, accSeq), (perm, cost) ->
if (accCost > cost) cost.toFloat() to perm
else accCost to accSeq
}
layered[i].clear()
layered[i].addAll(optPi.second.toMutableList())
}
}
// Assign x positions // Assign x positions
layered.forEachIndexed { idx, layer -> layered.forEachIndexed { idx, layer ->

View File

@ -59,6 +59,18 @@ sealed class SumType<out T1, out T2> {
is SumT2 -> onT2(value) is SumT2 -> onT2(value)
} }
/**
* Gets the contained value as a `T1`, or throws if the sum type holds a `T2`.
*/
val asT1: T1
get() = fold({ it }) { throw IllegalStateException("Not a T1: $this") }
/**
* Gets the contained value as a `T2`, or throws if the sum type holds a `T1`.
*/
val asT2: T2
get() = fold({ throw IllegalStateException("Not a T2: $this") }) { it }
override fun toString(): String = fold({ it.toString() }) { it.toString() } override fun toString(): String = fold({ it.toString() }) { it.toString() }
companion object { companion object {

View File

@ -0,0 +1,13 @@
package com.jaytux.altgraph.layout
fun <T> List<T>.permutations(): Sequence<List<T>> = sequence {
if(isEmpty()) yield(emptyList())
else {
for(i in indices) {
val elem = this@permutations[i]
for(perm in (this@permutations - elem).permutations()) {
yield(listOf(elem) + perm)
}
}
}
}

View File

@ -13,12 +13,12 @@ import kotlin.math.sqrt
class QuadraticEdge<E>( class QuadraticEdge<E>(
var delta: Float = 0.2f, var delta: Float = 0.2f,
var arrowLen: Int = 10, var arrowLen: Int = 10,
var color: Color = Color.BLACK, var color: (E) -> Color = { Color.BLACK },
var arrowAngle: Double = Math.PI / 6.0 var arrowAngle: Double = Math.PI / 6.0
) : IEdgeRenderer<E> { ) : IEdgeRenderer<E> {
override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) { override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) {
val g2 = g.create() as Graphics2D val g2 = g.create() as Graphics2D
g2.color = color g2.color = color(meta)
val len = from.distance(to).toFloat() val len = from.distance(to).toFloat()
val xLen = delta * len val xLen = delta * len