Layout Engines
The layout step computes positions for every diagram element. kUML ships two engines and lets third parties register more through a ServiceLoader SPI.
Two built-in engines
| Engine | Character |
|---|---|
|
Eclipse Layout Kernel Sugiyama-based layered layout. Default for class, component, deployment, and most C4 diagrams. Handles arbitrary graphs well, produces aesthetically reasonable output without user input. |
|
Pure-Kotlin grid engine, GraalVM-native-image-compatible. Deterministic, hint-driven — you place elements on a coarse grid; the engine respects your placement. Best when you want predictable, hand-tuned layouts. |
The engine is selected per render via LayoutEngineRegistry.pickFor(diagramKind, preferred):
-
If
preferredis set (CLI flag, config, or DSL hint), use it — no capability check. -
Otherwise, the first registered engine whose capabilities include the diagram type.
-
Otherwise, the first engine that supports
DiagramKind.Generic. -
Otherwise,
null(registry empty).
Override per render:
kuml render diagram.kuml.kts --engine kuml.grid
DSL hints — taking control
For predictable layouts, place a layout { … } block on any element. The hints
materialize into kuml.layout.* metadata, the bridge reads them into the engine’s
NodeHints, and the engine respects them (if capable).
classDiagram(name = "Domain") {
val customer = classOf("Customer") {
layout {
col = 0; row = 0
}
}
val order = classOf("Order") {
layout {
col = 1; row = 0
colSpan = 2
}
}
val item = classOf("OrderItem") {
layout {
below(order)
sameColAs(order)
}
}
}
Hint vocabulary:
| Hint | Effect |
|---|---|
|
Pin to column N (0-based). |
|
Pin to row N (0-based). |
|
Element spans N columns. |
|
Element spans N rows. |
|
Engine must not move this element from its current slot. |
|
Place above the referenced element. |
|
Place below the referenced element. |
|
Place to the left of the referenced element. |
|
Place to the right of the referenced element. |
|
Same row as the referenced element. |
|
Same column as the referenced element. |
References take either a typed val (preferred — IDE-checked) or a String ID:
val order = classOf("Order")
classOf("Item") { layout { below(order) } } // Typed reference
classOf("Item") { layout { below("Order") } } // String — looser, also works
When the engine can’t honour a hint
Conflicts produce a LayoutWarning in LayoutResult.warnings:
| Warning code | When |
|---|---|
|
Two elements want the same hard-pinned slot. Loser is shifted right; warning lists the affected node IDs. |
|
Preferred slot from a relative constraint was occupied. Fallback to next free slot. |
|
Anchor element wasn’t yet placed when the constraint was evaluated. |
|
Graph exceeds the engine’s recommended max ( |
The CLI prints warnings after a render so you can iterate on hints without staring at SVG diffs.
Edge routing
Four edge styles, all engines support all four:
| Style | Behaviour |
|---|---|
|
Straight line between source and target. |
|
Horizontal-vertical-horizontal (or vertical-horizontal-vertical) route with rounded corners. |
|
V-shape route with top-down bias — looks tidy for hierarchies. V1.1.12 falls back to
|
|
Cubic Bezier with two control points offset perpendicular to the direct line. |
Set per edge:
association(source = a, target = b) {
routeStyle = EdgeRouteStyle.Direct
}
Or globally for a diagram:
classDiagram(name = "X") {
layoutHints {
defaultEdgeStyle = EdgeRouteStyle.Bezier
}
// ...
}
Performance characteristics
| Size | elk.layered |
kuml.grid |
|---|---|---|
≤ 100 nodes |
< 100 ms |
< 10 ms |
≤ 500 nodes |
< 1 s |
< 100 ms |
> 500 nodes |
Still works, may be slow |
Emits |
The grid engine is deterministic — same input + same seed = byte-identical layout. ELK is also seeded but has more internal randomness in its heuristics; outputs are stable but small floating-point differences may appear across JVM versions.
Authoring your own engine
Implement KumlLayoutEngine (pure Kotlin recommended for native-image compatibility),
then provide a KumlLayoutEngineProvider:
class CircularLayoutEngine : KumlLayoutEngine {
override val id = LayoutEngineId("circular")
override val capabilities = LayoutCapabilities(
deterministic = true,
supportedDiagramKinds = setOf(DiagramKind.UmlClass, DiagramKind.Generic),
// ...
)
override fun layout(graph: LayoutGraph, hints: LayoutHints): LayoutResult {
// ... place nodes on a circle, route edges, return LayoutResult
}
}
class CircularLayoutEngineProvider : KumlLayoutEngineProvider {
override val id = LayoutEngineId("circular")
override fun engine() = CircularLayoutEngine()
}
Register via META-INF/services/dev.kuml.layout.KumlLayoutEngineProvider:
com.example.CircularLayoutEngineProvider
LayoutEngineRegistry.loadFromClasspath() discovers it. CLI flag --engine circular
selects it.