Small geometry overhaul

This commit is contained in:
2025-08-22 20:15:47 +02:00
parent 99d64242cf
commit 64560b172b
4 changed files with 40 additions and 67 deletions

View File

@ -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
}

View File

@ -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
} }

View File

@ -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!! }
} }

View File

@ -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"
}