Small geometry overhaul
This commit is contained in:
@ -1,13 +1,4 @@
|
|||||||
package com.jaytux.altgraph.layout
|
package com.jaytux.altgraph.layout
|
||||||
|
|
||||||
interface ISize<S : ISize<S>> {
|
data class GSize(var width: Float, var height: Float)
|
||||||
fun width(): Float
|
data class GPoint(var x: Float, var y: 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
|
|
||||||
}
|
|
@ -2,12 +2,12 @@ package com.jaytux.altgraph.layout
|
|||||||
|
|
||||||
import com.jaytux.altgraph.core.IGraph
|
import com.jaytux.altgraph.core.IGraph
|
||||||
|
|
||||||
interface ILayout<V, E, P : IPoint<P>, S : ISize<S>> {
|
interface ILayout<V, E> {
|
||||||
fun graph(): IGraph<V, E>
|
fun graph(): IGraph<V, E>
|
||||||
fun setGraph(graph: IGraph<V, E>)
|
fun setGraph(graph: IGraph<V, E>)
|
||||||
fun compute()
|
fun compute()
|
||||||
fun location(vertex: V): P
|
fun location(vertex: V): GPoint
|
||||||
fun freezeAt(vertex: V, point: P)
|
fun freezeAt(vertex: V, point: GPoint)
|
||||||
fun unfreeze(vertex: V)
|
fun unfreeze(vertex: V)
|
||||||
fun boundingBox(): S
|
fun boundingBox(): GSize
|
||||||
}
|
}
|
@ -8,37 +8,36 @@ import kotlin.concurrent.atomics.AtomicBoolean
|
|||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
class PseudoForestLayout<V, E>(
|
||||||
graph: IGraph<V, E>,
|
graph: IGraph<V, E>,
|
||||||
var horizontalMargin: Float,
|
var horizontalMargin: Float,
|
||||||
var disjoinXMargin: Float,
|
var disjoinXMargin: Float,
|
||||||
var interLayer: Float,
|
var interLayer: Float,
|
||||||
val pointZero: P, val sizeZero: S,
|
vertexSize: (V) -> VertexSize
|
||||||
vertexSize: (V) -> VertexSize<P, S>
|
) : ILayout<V, E> {
|
||||||
) : ILayout<V, E, P, S> {
|
data class VertexSize(val vertex: GSize, val labelOffset: GPoint, val labelSize: GSize) {
|
||||||
data class VertexSize<P : IPoint<P>, S : ISize<S>>(val vertex: S, val labelOffset: P, val labelSize: S) {
|
fun fullSize(): GSize { // TODO: check the math here
|
||||||
fun fullSize(): S { // TODO: check the math here
|
val minX = 0 + labelOffset.x
|
||||||
val minX = 0 + labelOffset.x()
|
val minY = 0 + labelOffset.y
|
||||||
val minY = 0 + labelOffset.y()
|
val maxX = vertex.width + labelOffset.x + labelSize.width
|
||||||
val maxX = vertex.width() + labelOffset.x() + labelSize.width()
|
val maxY = vertex.height + labelOffset.y + labelSize.height
|
||||||
val maxY = vertex.height() + labelOffset.y() + labelSize.height()
|
return GSize(
|
||||||
return vertex.copy(
|
|
||||||
width = maxX - minX,
|
width = maxX - minX,
|
||||||
height = maxY - minY
|
height = maxY - minY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun vCenterInBox(): P { // TODO: check the math here
|
fun vCenterInBox(): GPoint { // TODO: check the math here
|
||||||
return labelOffset.copy(
|
return GPoint(
|
||||||
x = labelOffset.x() + vertex.width() / 2,
|
x = labelOffset.x + vertex.width / 2,
|
||||||
y = labelOffset.y() + vertex.height() / 2
|
y = labelOffset.y + vertex.height / 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var _graph: IGraph<V, E> = graph
|
private var _graph: IGraph<V, E> = graph
|
||||||
private val _positions = mutableMapOf<V, P>()
|
private val _positions = mutableMapOf<V, GPoint>()
|
||||||
private var _vertexSize: (V) -> VertexSize<P, S> = vertexSize
|
private var _vertexSize: (V) -> VertexSize = vertexSize
|
||||||
private var _boundingBox: S? = null
|
private var _boundingBox: GSize? = null
|
||||||
|
|
||||||
@OptIn(ExperimentalAtomicApi::class)
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
private var _lock: AtomicBoolean = AtomicBoolean(false)
|
||||||
@ -61,7 +60,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
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 } }
|
||||||
fun setVertexSize(vertexSize: (V) -> VertexSize<P, S>) { 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
|
// Either a vertex, or a dummy node to break up multi-layer-spanning edges
|
||||||
private data class Connector<V>(
|
private data class Connector<V>(
|
||||||
@ -120,7 +119,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
// Assign a layer to each vertex by traversing depth-first and ignoring back-edges.
|
||||||
val roots = _graph.roots()
|
val roots = _graph.roots()
|
||||||
if (roots.isEmpty()) { // Only reachable nodes matter.
|
if (roots.isEmpty()) { // Only reachable nodes matter.
|
||||||
_boundingBox = sizeZero
|
_boundingBox = GSize(0.0f, 0.0f)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
val layers = mutableMapOf<V, Pair<Int, MutableSet<V>>>()
|
||||||
@ -162,10 +161,10 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
layers.forEach { (vertex, pair) ->
|
layers.forEach { (vertex, pair) ->
|
||||||
val (layer, _) = pair
|
val (layer, _) = pair
|
||||||
val size = vertexSizes[vertex]!!
|
val size = vertexSizes[vertex]!!
|
||||||
layerHeights[layer] = max(layerHeights[layer], size.first.height())
|
layerHeights[layer] = max(layerHeights[layer], size.first.height)
|
||||||
if(layer == 0) {
|
if(layer == 0) {
|
||||||
// Take into account vertex bounding box offset
|
// Take into account vertex bounding box offset
|
||||||
val delta = size.second.y()
|
val delta = size.second.y
|
||||||
if(delta < minOffset) minOffset = delta
|
if(delta < minOffset) minOffset = delta
|
||||||
if(delta > maxOffset) maxOffset = delta
|
if(delta > maxOffset) maxOffset = delta
|
||||||
}
|
}
|
||||||
@ -234,7 +233,7 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
layered[i].forEach { v ->
|
layered[i].forEach { v ->
|
||||||
v.x.fold({ node ->
|
v.x.fold({ node ->
|
||||||
val w = vertexSizes[node]!!.first.width()
|
val w = vertexSizes[node]!!.first.width
|
||||||
layerWidths[i] += w + horizontalMargin
|
layerWidths[i] += w + horizontalMargin
|
||||||
}) { /* do nothing */ }
|
}) { /* do nothing */ }
|
||||||
}
|
}
|
||||||
@ -251,8 +250,8 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
layer.forEach { v ->
|
layer.forEach { v ->
|
||||||
v.x.fold({ node ->
|
v.x.fold({ node ->
|
||||||
val offset = vertexSizes[node]!!.second
|
val offset = vertexSizes[node]!!.second
|
||||||
_positions[node] = pointZero.copy(x = currentX + offset.x(), y = layerY[idx] + offset.y())
|
_positions[node] = GPoint(x = currentX + offset.x, y = layerY[idx] + offset.y)
|
||||||
currentX += vertexSizes[node]!!.first.width() + horizontalMargin
|
currentX += vertexSizes[node]!!.first.width + horizontalMargin
|
||||||
}) { /* do nothing */ }
|
}) { /* do nothing */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,11 +262,11 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
|
|
||||||
// Compute the bounding box
|
// Compute the bounding box
|
||||||
// min x and y are 0
|
// min x and y are 0
|
||||||
_boundingBox = sizeZero.copy(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
_boundingBox = GSize(currentXZero - disjoinXMargin, layerY.last() + layerHeights.last() / 2 + offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun location(vertex: V): P {
|
override fun location(vertex: V): GPoint {
|
||||||
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
if(vertex !in _graph.vertices()) throw GraphException.vertexNotFound(vertex)
|
||||||
return _positions[vertex] ?: run {
|
return _positions[vertex] ?: run {
|
||||||
compute()
|
compute()
|
||||||
@ -275,8 +274,8 @@ class PseudoForestLayout<V, E, P : IPoint<P>, S : ISize<S>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun freezeAt(vertex: V, point: P) = throw UnsupportedOperationException("PseudoForestLayout does not allow freezing vertices.")
|
override fun freezeAt(vertex: V, point: GPoint) = throw UnsupportedOperationException("PseudoForestLayout does not allow freezing vertices.")
|
||||||
override fun unfreeze(vertex: V) { /* no-op: cannot freeze vertices */ }
|
override fun unfreeze(vertex: V) { /* no-op: cannot freeze vertices */ }
|
||||||
|
|
||||||
override fun boundingBox(): S = _boundingBox ?: run { compute(); _boundingBox!! }
|
override fun boundingBox(): GSize = _boundingBox ?: run { compute(); _boundingBox!! }
|
||||||
}
|
}
|
@ -1,29 +1,12 @@
|
|||||||
package com.jaytux.altgraph.swing
|
package com.jaytux.altgraph.swing
|
||||||
|
|
||||||
import com.jaytux.altgraph.layout.IPoint
|
import com.jaytux.altgraph.layout.GPoint
|
||||||
import com.jaytux.altgraph.layout.ISize
|
import com.jaytux.altgraph.layout.GSize
|
||||||
import java.awt.Dimension
|
import java.awt.Dimension
|
||||||
import java.awt.Point
|
import java.awt.Point
|
||||||
|
|
||||||
class GSize(val size: Dimension) : ISize<GSize> {
|
fun GSize.swing() = Dimension(width.toInt(), height.toInt())
|
||||||
override fun width(): Float = size.width.toFloat()
|
fun GPoint.swing() = Point(x.toInt(), y.toInt())
|
||||||
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()
|
fun Dimension.graph() = GSize(width.toFloat(), height.toFloat())
|
||||||
override fun equals(other: Any?): Boolean = other is GSize && size == other.size
|
fun Point.graph() = GPoint(x.toFloat(), y.toFloat())
|
||||||
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