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
SysML 2 layout hints
Every SysML 2 diagram accepts a layout { } block with coarse steering hints. There are deliberately
no pixel positions and no pairwise relative constraints.
reqDiagram("Requirements") {
// include(...)
layout {
direction = Right // Down (default), Right, Up, Left
wrapColumns = 4 // at most 4 nodes per layer
sameLayer(a, b) // same layer
first(root) // first layer
last(leaf) // last layer
}
}
-
wrapColumnsmoves the nodes of an over-full layer (declaration order) into additional layers. Layers are computed by longest-path layering; back edges are ignored. -
firstandlastuse ELK’s layer constraints when no wrap/sameLayeris given and the edges allow it (afirstnode must have no incoming and alastnode no outgoing edge, otherwise the pin is dropped). Together with wrapping they are expressed as layer indices. -
Layer hints are honoured for nodes that are not inside a compound group. ELK ignores them inside compounds and reports
hint.ignored.layerInCompound. -
Sequence diagrams ignore all hints (lifeline order is the declaration order);
kuml validatewarns (LAYOUT412). -
The grid engine and the WebAssembly playground do not evaluate these hints.
-
Swimlane (partition) activity diagrams are drawn top to bottom only; another
directionis reported asLAYOUT433. -
Checks:
LAYOUT431unknown id,LAYOUT432wrapColumnsoutside 1..32,LAYOUT434too many groups/ids,LAYOUT435id infirstandlast,LAYOUT436connected elements in onesameLayergroup.
Example: a wide requirement diagram
Without a hint, twelve requirements are laid out in one very wide row:
import dev.kuml.sysml2.RequirementPriority
import dev.kuml.sysml2.dsl.sysml2Model
sysml2Model("Platform Requirements") {
val reqs = listOf(
"Boot" to "The platform shall boot within 5 seconds.",
"Update" to "Updates shall install without user interaction.",
"Rollback" to "A failed update shall roll back automatically.",
"Telemetry" to "Telemetry shall be opt-in.",
"Logging" to "Logs shall be kept for 30 days.",
"Auth" to "Every request shall be authenticated.",
"Audit" to "Security events shall be audited.",
"Encrypt" to "Data at rest shall be encrypted.",
"Backup" to "Backups shall run nightly.",
"Restore" to "A restore shall complete within one hour.",
"Scale" to "The platform shall scale to 10 000 users.",
"Monitor" to "Health shall be monitored continuously.",
).mapIndexed { i, (name, text) ->
requirementDef(name, reqId = "R-${i + 1}", text = text, priority = RequirementPriority.MEDIUM)
}
reqDiagram("Platform requirements") {
reqs.forEach { include(it) }
}
}
With layout { wrapColumns = 4 } the same diagram wraps into three rows of four (vault source:
03 Bereiche/kUML/Beispiele/52 SysML 2 REQ – Breites Diagramm mit wrapColumns.md):
import dev.kuml.sysml2.RequirementPriority
import dev.kuml.sysml2.dsl.sysml2Model
sysml2Model("Platform Requirements") {
val reqs = listOf(
"Boot" to "The platform shall boot within 5 seconds.",
"Update" to "Updates shall install without user interaction.",
"Rollback" to "A failed update shall roll back automatically.",
"Telemetry" to "Telemetry shall be opt-in.",
"Logging" to "Logs shall be kept for 30 days.",
"Auth" to "Every request shall be authenticated.",
"Audit" to "Security events shall be audited.",
"Encrypt" to "Data at rest shall be encrypted.",
"Backup" to "Backups shall run nightly.",
"Restore" to "A restore shall complete within one hour.",
"Scale" to "The platform shall scale to 10 000 users.",
"Monitor" to "Health shall be monitored continuously.",
).mapIndexed { i, (name, text) ->
requirementDef(name, reqId = "R-${i + 1}", text = text, priority = RequirementPriority.MEDIUM)
}
reqDiagram("Platform requirements") {
reqs.forEach { include(it) }
layout {
wrapColumns = 4
}
}
}
All SysML 2 render paths (CLI, Gradle, Web, Desktop, Asciidoc, kuml-ai) take their ELK hints from the single function
Sysml2LayoutBridge.layoutHintsFor.
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.