Rendering
This commit is contained in:
@ -2,7 +2,7 @@ plugins {
|
||||
kotlin("jvm") version "2.2.0"
|
||||
}
|
||||
|
||||
group = "be.topl.phoenix-intellij"
|
||||
group = "com.jaytux.altgraph"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
|
51
src/main/kotlin/com/jaytux/altgraph/examples/SimpleCFG.kt
Normal file
51
src/main/kotlin/com/jaytux/altgraph/examples/SimpleCFG.kt
Normal file
@ -0,0 +1,51 @@
|
||||
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 javax.swing.JFrame
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
fun getGraph(): IGraph<String, Int> {
|
||||
val graph = BaseGraph<String, Int>()
|
||||
// vertices
|
||||
val entry = graph.addVertex("[ENTRY]", true)
|
||||
val exit = graph.addVertex("[EXIT]")
|
||||
val labels = intArrayOf(198, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210).associateWith { graph.addVertex(".label_$it") }
|
||||
|
||||
var count = 0
|
||||
graph.connect(entry, labels[198]!!, count++)
|
||||
arrayOf(
|
||||
198 to 200, 200 to 201, 200 to 202, 201 to 203, 201 to 204, 203 to 200, 204 to 205, 205 to 208, 208 to 209,
|
||||
209 to 200, 209 to 210, 210 to 208, 204 to 206, 206 to 207, 207 to 208
|
||||
).forEach { (from, to) -> graph.connect(labels[from]!!, labels[to]!!, count++) }
|
||||
graph.connect(labels[202]!!, exit, count++)
|
||||
return graph
|
||||
}
|
||||
|
||||
fun getPane(graph: IGraph<String, Int>): GraphPane<String, Int> {
|
||||
val pane = GraphPane(graph) { pane, graph ->
|
||||
PseudoForestLayout(graph, 10.0f, 20.0f, 10.0f) { v ->
|
||||
(pane.getComponentFor(v) as DefaultVertexComponent).vertexSize()
|
||||
}
|
||||
}
|
||||
return pane
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val graph = getGraph()
|
||||
val pane = getPane(graph)
|
||||
|
||||
// show pane in window
|
||||
SwingUtilities.invokeLater {
|
||||
val frame = JFrame("Simple Control Flow Graph")
|
||||
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
|
||||
frame.setSize(800, 600)
|
||||
frame.add(pane)
|
||||
frame.pack()
|
||||
frame.setLocationRelativeTo(null)
|
||||
frame.isVisible = true
|
||||
}
|
||||
}
|
@ -18,4 +18,12 @@ data class GSize(var width: Float, var height: Float)
|
||||
* @property x the x coordinate
|
||||
* @property y the y coordinate
|
||||
*/
|
||||
data class GPoint(var x: Float, var y: Float)
|
||||
data class GPoint(var x: Float, var y: Float) {
|
||||
/**
|
||||
* Adds two points component-wise.
|
||||
*
|
||||
* @param other the other point to add
|
||||
* @return a new point representing the sum of this point and [other]
|
||||
*/
|
||||
operator fun plus(other: GPoint): GPoint = GPoint(x + other.x, y + other.y)
|
||||
}
|
@ -96,11 +96,13 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
|
||||
try {
|
||||
println("Took lock in ${Exception().stackTrace.first()}")
|
||||
val x = block()
|
||||
_lock.store(false) // unlock after operation
|
||||
return x
|
||||
}
|
||||
finally {
|
||||
println("Releasing lock in ${Exception().stackTrace.first()}")
|
||||
_lock.store(false) // we can safely unlock
|
||||
}
|
||||
}
|
||||
@ -166,7 +168,9 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
|
||||
override fun compute() {
|
||||
println("Acquiring lock")
|
||||
locked {
|
||||
print("Starting layouting")
|
||||
_positions.clear()
|
||||
|
||||
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
||||
@ -178,17 +182,22 @@ class PseudoForestLayout<V, E>(
|
||||
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() })
|
||||
println(" - Computing layers from roots: $roots")
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
val (vertex, onPath) = queue.removeFirst()
|
||||
val (layer, dep) = layers.getOrPut(vertex) { 0 to mutableSetOf() }
|
||||
println(" - Visiting $vertex (layer $layer), path=$onPath, deps=$dep")
|
||||
|
||||
val succLayer = layer + 1
|
||||
_graph.successors(vertex).forEach { (succ, _) ->
|
||||
if (succ in onPath) return@forEach
|
||||
dep += succ
|
||||
|
||||
if (succ in onPath) return@forEach
|
||||
layers[succ]?.let { (l, sDep) ->
|
||||
println(" - Successor $succ already had layer $l (might be increased, along with its dependents)")
|
||||
dep += sDep
|
||||
|
||||
val delta = succLayer - l
|
||||
if (delta > 0) {
|
||||
layers[succ] = succLayer to (sDep)
|
||||
@ -197,17 +206,26 @@ class PseudoForestLayout<V, E>(
|
||||
?: (succLayer to mutableSetOf())
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
layers[succ] = succLayer to mutableSetOf()
|
||||
}
|
||||
println(" - Adding successor to queue: $succ (layer: ${layers[succ]?.first})")
|
||||
queue.addLast(succ to onPath + vertex)
|
||||
}
|
||||
|
||||
// ensure dependents are always up to date
|
||||
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
|
||||
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 layerCount = layers.maxOf { it.value.first } + 1
|
||||
val layerHeights = MutableList(layerCount) { 0.0f }
|
||||
println(" - Have $layerCount layers")
|
||||
|
||||
var minOffset = Float.POSITIVE_INFINITY
|
||||
var maxOffset = Float.NEGATIVE_INFINITY
|
||||
@ -230,9 +248,17 @@ class PseudoForestLayout<V, E>(
|
||||
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
|
||||
val disjoint = roots.fold(listOf<Set<V>>()) { acc, root ->
|
||||
val reachable = layers[root]?.second?.toMutableSet() ?: return@fold acc
|
||||
reachable += root
|
||||
|
||||
val dedup = acc.mapNotNull { other ->
|
||||
val inter = reachable intersect other
|
||||
@ -248,6 +274,7 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
var currentXZero = 0.0f
|
||||
disjoint.forEach { sub ->
|
||||
println(" - Layouting disjoint subgraph: $sub")
|
||||
// Put each vertex in a list by layer
|
||||
val layered = List(layerCount) { layer ->
|
||||
sub.mapNotNull {
|
||||
@ -256,6 +283,9 @@ class PseudoForestLayout<V, E>(
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
println(" - Initial layered vertices:")
|
||||
layered.forEachIndexed { idx, list -> println(" - Layer $idx: $list") }
|
||||
|
||||
// Break up multi-layer edges with dummy nodes
|
||||
layered.forEachIndexed { idx, list ->
|
||||
list.forEach { v ->
|
||||
@ -275,6 +305,15 @@ class PseudoForestLayout<V, E>(
|
||||
}
|
||||
|
||||
val layerWidths = MutableList(layerCount) { -horizontalMargin } // avoid double adding margin on 1st node
|
||||
|
||||
// Layout roots
|
||||
layered[0].forEach { root ->
|
||||
root.x.fold({ node ->
|
||||
val w = vertexSizes[node]!!.first.width
|
||||
layerWidths[0] += w + horizontalMargin
|
||||
}) {}
|
||||
}
|
||||
|
||||
// Layer-by-layer, assign x slots (not yet positions)
|
||||
for(i in 1 until layered.size) {
|
||||
// Barycenter heuristic: average of parents' slots
|
||||
@ -299,11 +338,13 @@ class PseudoForestLayout<V, E>(
|
||||
|
||||
// Assign x positions
|
||||
layered.forEachIndexed { idx, layer ->
|
||||
println(" - Positioning layer $idx")
|
||||
var currentX = currentXZero + (maxWidth - layerWidths[idx]) / 2
|
||||
layer.forEach { v ->
|
||||
v.x.fold({ node ->
|
||||
val offset = vertexSizes[node]!!.second
|
||||
_positions[node] = GPoint(x = currentX + offset.x, y = layerY[idx] + offset.y)
|
||||
println(" - Put vertex $node at ${_positions[node]}")
|
||||
currentX += vertexSizes[node]!!.first.width + horizontalMargin
|
||||
}) { /* do nothing */ }
|
||||
}
|
||||
@ -316,16 +357,16 @@ class PseudoForestLayout<V, E>(
|
||||
// Compute the bounding box
|
||||
// min x and y are 0
|
||||
_boundingBox = GSize(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
||||
println("Done layouting")
|
||||
}
|
||||
println("Released lock")
|
||||
}
|
||||
|
||||
override fun location(vertex: V): GPoint {
|
||||
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||
return locked {
|
||||
_positions[vertex] ?: run {
|
||||
compute()
|
||||
_positions[vertex]!!
|
||||
}
|
||||
if (vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||
return _positions[vertex] ?: run {
|
||||
compute()
|
||||
_positions[vertex] ?: throw IllegalArgumentException("Vertex $vertex was not layout-ed: $_positions")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,8 @@ sealed class SumType<out T1, out T2> {
|
||||
is SumT2 -> onT2(value)
|
||||
}
|
||||
|
||||
override fun toString(): String = fold({ it.toString() }) { it.toString() }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Constructs a [SumType] holding a `T1`.
|
||||
|
66
src/main/kotlin/com/jaytux/altgraph/swing/DefaultEdge.kt
Normal file
66
src/main/kotlin/com/jaytux/altgraph/swing/DefaultEdge.kt
Normal file
@ -0,0 +1,66 @@
|
||||
package com.jaytux.altgraph.swing
|
||||
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Point
|
||||
import java.awt.Polygon
|
||||
import java.awt.geom.QuadCurve2D
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class QuadraticEdge<E>(
|
||||
var delta: Float = 0.2f,
|
||||
var arrowLen: Int = 10,
|
||||
var color: Color = Color.BLACK,
|
||||
var arrowAngle: Double = Math.PI / 6.0
|
||||
) : IEdgeRenderer<E> {
|
||||
override fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float) {
|
||||
val g2 = g.create() as Graphics2D
|
||||
g2.color = color
|
||||
|
||||
val len = from.distance(to).toFloat()
|
||||
val xLen = delta * len
|
||||
|
||||
val fx = from.x.toFloat(); val fy = from.y.toFloat()
|
||||
val tx = to.x.toFloat(); val ty = to.y.toFloat()
|
||||
|
||||
val midX = (fx + tx) / 2
|
||||
val midY = (fy + ty) / 2
|
||||
|
||||
|
||||
val x1 = tx - midX; val y1 = ty - midY
|
||||
val fac = xLen / sqrt(x1 * x1 + y1 * y1)
|
||||
|
||||
val cx = (midX + y1 * fac)
|
||||
val cy = (midY - x1 * fac)
|
||||
|
||||
val dx1 = cx - fx; val dy1 = cy - fy; val d1fac = offsetFrom / sqrt(dx1 * dx1 + dy1 * dy1)
|
||||
val dx2 = tx - cx; val dy2 = ty - cy; val d2fac = offsetTo / sqrt(dx2 * dx2 + dy2 * dy2)
|
||||
|
||||
val x1_ = fx + dx1 * d1fac; val y1_ = fy + dy1 * d1fac
|
||||
val x2_ = tx - dx2 * d2fac; val y2_ = ty - dy2 * d2fac
|
||||
val curve = QuadCurve2D.Float(
|
||||
x1_, y1_,
|
||||
cx, cy,
|
||||
x2_, y2_
|
||||
)
|
||||
g2.draw(curve)
|
||||
|
||||
// draw arrowhead at the end of the curve
|
||||
val angle = atan2(y2_ - cy, x2_ - cx)
|
||||
val arrowX1 = x2_ - arrowLen * cos(angle - arrowAngle).toFloat()
|
||||
val arrowY1 = y2_ - arrowLen * sin(angle - arrowAngle).toFloat()
|
||||
val arrowX2 = x2_ - arrowLen * cos(angle + arrowAngle).toFloat()
|
||||
val arrowY2 = y2_ - arrowLen * sin(angle + arrowAngle).toFloat()
|
||||
val arrowHead = Polygon(
|
||||
intArrayOf(x2_.toInt(), arrowX1.toInt(), arrowX2.toInt()),
|
||||
intArrayOf(y2_.toInt(), arrowY1.toInt(), arrowY2.toInt()),
|
||||
3
|
||||
)
|
||||
g2.fill(arrowHead)
|
||||
}
|
||||
}
|
||||
|
||||
fun <E> defaultEdgeRenderer(): IEdgeRenderer<E> = QuadraticEdge()
|
@ -0,0 +1,87 @@
|
||||
package com.jaytux.altgraph.swing
|
||||
|
||||
import com.jaytux.altgraph.layout.PseudoForestLayout
|
||||
import java.awt.*
|
||||
import java.awt.RenderingHints.KEY_ANTIALIASING
|
||||
import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||
import java.awt.geom.AffineTransform
|
||||
import java.awt.geom.Ellipse2D
|
||||
import javax.swing.JLabel
|
||||
|
||||
class DefaultVertexComponent(
|
||||
label: String,
|
||||
private var _shape: Shape = Ellipse2D.Double(0.0, 0.0, 30.0, 30.0),
|
||||
var labelOffset: Point = Point(20, 7),
|
||||
) : IDrawable {
|
||||
val label = JLabel(label)
|
||||
private lateinit var _preferredSize: Dimension
|
||||
private var _arrowTarget: Point = Point(0, 0)
|
||||
private var _arrowTargetOffset: Float = 0.0f
|
||||
var fillColor: Color = Color.LIGHT_GRAY
|
||||
var borderStroke: Stroke = BasicStroke(1.0f)
|
||||
var borderColor: Color = Color.BLACK
|
||||
|
||||
var shape: Shape
|
||||
get() = _shape
|
||||
set(value) {
|
||||
_shape = value
|
||||
_arrowTarget = Point(value.bounds.width / 2, value.bounds.height / 2)
|
||||
_arrowTargetOffset = (value.bounds.width + value.bounds.height).toFloat() / 4.0f
|
||||
updatePreferredSize()
|
||||
}
|
||||
|
||||
init {
|
||||
updatePreferredSize()
|
||||
shape = _shape // to initialize arrow target and offset
|
||||
}
|
||||
|
||||
fun updatePreferredSize() {
|
||||
val bounds = shape.bounds
|
||||
val textSize = label.preferredSize
|
||||
|
||||
val cx = bounds.width / 2; val cy = bounds.height / 2
|
||||
val tx = cx + labelOffset.x; val ty = cy + labelOffset.y - textSize.height / 2
|
||||
|
||||
val textRect = Rectangle(tx, ty, textSize.width, textSize.height)
|
||||
val total = bounds.union(textRect)
|
||||
_preferredSize = Dimension(total.width, total.height)
|
||||
}
|
||||
|
||||
override fun draw(graphics: Graphics2D) {
|
||||
val g = graphics.create() as Graphics2D
|
||||
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON)
|
||||
val shapeBounds = shape.bounds
|
||||
val cx = shapeBounds.width / 2; val cy = shapeBounds.height / 2
|
||||
val tx = AffineTransform.getTranslateInstance((cx - shapeBounds.centerX), (cy - shapeBounds.centerY))
|
||||
val transShape = tx.createTransformedShape(shape)
|
||||
|
||||
g.color = fillColor
|
||||
g.fill(transShape)
|
||||
g.color = borderColor
|
||||
g.stroke = borderStroke
|
||||
g.draw(transShape)
|
||||
|
||||
val textSize = label.preferredSize
|
||||
val lx = cx + labelOffset.x; val ly = cy + labelOffset.y - textSize.height / 2
|
||||
label.size = textSize
|
||||
g.translate(lx, ly)
|
||||
label.paint(g)
|
||||
|
||||
g.dispose()
|
||||
}
|
||||
|
||||
fun vertexSize(): PseudoForestLayout.VertexSize {
|
||||
val size = _preferredSize
|
||||
val lSize = label.preferredSize
|
||||
return PseudoForestLayout.VertexSize(size.graph(), labelOffset.graph(), lSize.graph())
|
||||
}
|
||||
|
||||
fun asRenderData(): IVertexRenderer.RenderData =
|
||||
IVertexRenderer.RenderData(this, _arrowTarget, _arrowTargetOffset)
|
||||
}
|
||||
|
||||
fun <V> defaultRenderer(
|
||||
toString: (V) -> String = { it.toString() }
|
||||
) : IVertexRenderer<V> = IVertexRenderer { v: V ->
|
||||
DefaultVertexComponent(toString(v)).asRenderData()
|
||||
}
|
21
src/main/kotlin/com/jaytux/altgraph/swing/Functional.kt
Normal file
21
src/main/kotlin/com/jaytux/altgraph/swing/Functional.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package com.jaytux.altgraph.swing
|
||||
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Point
|
||||
|
||||
interface IDrawable {
|
||||
fun draw(graphics: Graphics2D)
|
||||
}
|
||||
|
||||
fun interface IVertexRenderer<V> {
|
||||
data class RenderData(
|
||||
val drawer: IDrawable,
|
||||
val arrowTarget: Point,
|
||||
val arrowTargetOffset: Float
|
||||
)
|
||||
fun getDrawable(v: V): RenderData
|
||||
}
|
||||
|
||||
fun interface IEdgeRenderer<E> {
|
||||
fun drawEdge(g: Graphics2D, from: Point, to: Point, meta: E, offsetFrom: Float, offsetTo: Float)
|
||||
}
|
131
src/main/kotlin/com/jaytux/altgraph/swing/GraphPane.kt
Normal file
131
src/main/kotlin/com/jaytux/altgraph/swing/GraphPane.kt
Normal file
@ -0,0 +1,131 @@
|
||||
package com.jaytux.altgraph.swing
|
||||
|
||||
import com.jaytux.altgraph.core.IGraph
|
||||
import com.jaytux.altgraph.layout.ILayout
|
||||
import java.awt.Graphics
|
||||
import java.awt.Graphics2D
|
||||
import java.awt.Point
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.MouseMotionListener
|
||||
import java.awt.geom.AffineTransform
|
||||
import javax.swing.JPanel
|
||||
|
||||
class GraphPane<V, E>(
|
||||
val graph: IGraph<V, E>,
|
||||
layout: (GraphPane<V, E>, IGraph<V, E>) -> ILayout<V, E>
|
||||
) : JPanel()
|
||||
{
|
||||
private val _delta = AffineTransform()
|
||||
private var _zoom = 1.0
|
||||
private var _panX = 0.0
|
||||
private var _panY = 0.0
|
||||
|
||||
private val _layout: ILayout<V, E>
|
||||
private var _renderer: IVertexRenderer<V> = defaultRenderer()
|
||||
private var _edgeRenderer: IEdgeRenderer<E> = defaultEdgeRenderer()
|
||||
private val _vertices = mutableMapOf<V, IVertexRenderer.RenderData>()
|
||||
|
||||
init {
|
||||
this.layout = null
|
||||
_delta.setToIdentity()
|
||||
_layout = layout(this, graph)
|
||||
|
||||
addMouseWheelListener { event ->
|
||||
val delta = event.preciseWheelRotation
|
||||
if(delta < 0) {
|
||||
_zoom *= 1.1
|
||||
} else {
|
||||
_zoom /= 1.1
|
||||
}
|
||||
transform()
|
||||
}
|
||||
|
||||
addMouseMotionListener(object : MouseMotionListener {
|
||||
var last: Point? = null
|
||||
|
||||
override fun mouseDragged(e: MouseEvent?) {
|
||||
e?.let {
|
||||
last?.let {
|
||||
val dx = e.x - it.x
|
||||
val dy = e.y - it.y
|
||||
_panX += dx
|
||||
_panY += dy
|
||||
transform()
|
||||
}
|
||||
|
||||
last = Point(e.x, e.y)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseMoved(e: MouseEvent?) {
|
||||
e?.let { last = it.point }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun transform() {
|
||||
_delta.setToIdentity()
|
||||
_delta.translate(_panX, _panY)
|
||||
_delta.scale(_zoom, _zoom)
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun relayout() {
|
||||
_layout.compute()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun getComponentFor(v: V): IDrawable =
|
||||
getRenderDataFor(v).drawer
|
||||
|
||||
fun getRenderDataFor(v: V): IVertexRenderer.RenderData =
|
||||
_vertices.getOrPut(v) { _renderer.getDrawable(v) }
|
||||
|
||||
fun setRenderer(renderer: IVertexRenderer<V>) {
|
||||
_renderer = renderer
|
||||
_vertices.clear()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun setEdgeRenderer(renderer: IEdgeRenderer<E>) {
|
||||
_edgeRenderer = renderer
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun resetComponents() {
|
||||
_vertices.clear()
|
||||
repaint()
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics?) {
|
||||
super.paintComponent(g)
|
||||
if(g == null) return
|
||||
|
||||
val g2 = g.create() as Graphics2D
|
||||
g2.transform = _delta
|
||||
graph.vertices().forEach { v -> getRenderDataFor(v) } // ensure all vertices are rendered
|
||||
_vertices.forEach { (v, c) ->
|
||||
if(v !in graph.vertices()) return@forEach
|
||||
val pos = _layout.location(v).swing()
|
||||
|
||||
val g3 = g2.create() as Graphics2D
|
||||
g3.translate(pos.x, pos.y)
|
||||
c.drawer.draw(g3)
|
||||
g3.dispose()
|
||||
}
|
||||
|
||||
graph.edges().forEach { (e, vv) ->
|
||||
val first = getRenderDataFor(vv.first)
|
||||
val second = getRenderDataFor(vv.second)
|
||||
val from = _layout.location(vv.first) + first.arrowTarget.graph()
|
||||
val to = _layout.location(vv.second) + second.arrowTarget.graph()
|
||||
|
||||
_edgeRenderer.drawEdge(g2,
|
||||
from.swing(),
|
||||
to.swing(),
|
||||
e, first.arrowTargetOffset, second.arrowTargetOffset)
|
||||
}
|
||||
|
||||
g2.dispose()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user