diff --git a/docs/design/rendering-layout/engine/interconnection-layout-engine.md b/docs/design/rendering-layout/engine/interconnection-layout-engine.md
index 3d61a45..a6d7e31 100644
--- a/docs/design/rendering-layout/engine/interconnection-layout-engine.md
+++ b/docs/design/rendering-layout/engine/interconnection-layout-engine.md
@@ -6,10 +6,11 @@ Part of the Rendering Layout system.
`InterconnectionLayoutEngine` places directed graphs and routes all connector lines using a full
Sugiyama-style pipeline. It is a thin facade that assembles and runs the reusable
-`LayeredLayoutPipeline` (see *Layered Pipeline*) with its default stage sequence, the Right layout
-direction, and flat hierarchy handling. Its `Place` API and `LayerResult` output are the stable
-internal contract; the facade produces byte-for-byte identical geometry to the previous monolithic
-implementation, proven by an equivalence test against a legacy oracle.
+`LayeredLayoutPipeline` (see *Layered Pipeline*) with its default stage sequence, the requested layout
+direction (defaulting to Right), and flat hierarchy handling. Its `Place` API and `LayerResult` output
+are the stable internal contract; for the default Right direction the facade produces byte-for-byte
+identical geometry to the previous monolithic implementation, proven by an equivalence test against a
+legacy oracle.
## InterconnectionLayoutEngine Data Model
@@ -22,13 +23,21 @@ with `ConnectorWaypoints`.
## InterconnectionLayoutEngine Methods
-`Place(nodes, edges)` builds a `LayeredGraph` from the inputs, assembles a `LayeredLayoutPipeline`
-with the default stages, and runs it. It then reads the placed coordinates, column extents, layer
-assignments, and waypoints from the graph state and assembles the `LayerResult`. Because the
-pipeline drops self-loops, de-duplicates identical directed pairs, and reverses back edges,
-`ConnectorWaypoints` holds one polyline per acyclic edge; consumers key a `(source, target)` lookup
-on `AcyclicEdges` (reversing the polyline for a reversed back edge) to recover each input edge's
-route.
+`Place(nodes, edges, direction)` builds a `LayeredGraph` from the inputs, assembles a
+`LayeredLayoutPipeline` with the default stages for the requested `direction` (defaulting to Right),
+and runs it. It then reads the placed coordinates, column extents, layer assignments, and waypoints
+from the graph state and assembles the `LayerResult`. Because the pipeline drops self-loops,
+de-duplicates identical directed pairs, and reverses back edges, `ConnectorWaypoints` holds one
+polyline per acyclic edge; consumers key a `(source, target)` lookup on `AcyclicEdges` (reversing the
+polyline for a reversed back edge) to recover each input edge's route.
+
+The reported total dimensions are direction-aware. The pipeline's axis-transform stage places the
+nodes along the requested direction, so a top-to-bottom (Down) or bottom-to-top (Up) flow transposes
+the layout relative to the left-to-right (Right) and right-to-left (Left) flows. The engine computes
+the along-axis extent from the column geometry and the cross-axis extent from the placed screen
+coordinates, then assigns them to `TotalWidth`/`TotalHeight` per direction: for Right/Left the
+along-axis is the width and for Down/Up it is the height. The Right path is unchanged and remains
+byte-identical.
## InterconnectionLayoutEngine Error Handling
@@ -51,4 +60,5 @@ strategy to obtain a placement result.
| Rendering-Layout-InterconnectionEngine-NonOverlapping | InterconnectionLayoutEngine behavior described above |
| Rendering-Layout-InterconnectionEngine-DummyNodes | InterconnectionLayoutEngine behavior described above |
| Rendering-Layout-InterconnectionEngine-Waypoints | InterconnectionLayoutEngine behavior described above |
+| Rendering-Layout-InterconnectionEngine-Direction | InterconnectionLayoutEngine behavior described above |
| Rendering-Layout-InterconnectionEngine-Deterministic | InterconnectionLayoutEngine behavior described above |
diff --git a/docs/design/rendering-layout/layered-layout-algorithm.md b/docs/design/rendering-layout/layered-layout-algorithm.md
index f654396..08caa92 100644
--- a/docs/design/rendering-layout/layered-layout-algorithm.md
+++ b/docs/design/rendering-layout/layered-layout-algorithm.md
@@ -25,10 +25,17 @@ per node followed by `LayoutLine` per edge).
`LayerNode` array from node sizes.
2. **Edge mapping.** Maps each edge to an index pair, dropping any edge that references a node
outside this graph.
-3. **Placement.** Calls `InterconnectionLayoutEngine.Place` to obtain the `LayerResult`.
-4. **Box emission.** Emits one `LayoutBox` per input node, in input order, at the placed rectangle,
+3. **Direction resolution.** Resolves the requested flow direction from `CoreOptions.Direction`,
+ taking an explicit value on the graph in preference to one on the options and falling back to the
+ property default (`Right`), then maps the public `LayoutFlowDirection` to the engine's internal
+ direction. This mirrors how the algorithm resolves its other well-known options.
+4. **Placement.** Calls `InterconnectionLayoutEngine.Place` with the resolved direction to obtain the
+ `LayerResult`. For a downward or upward flow the engine transposes the layout so the layers
+ progress top-to-bottom (or bottom-to-top); `Right` is the default and is byte-identical to the
+ original left-to-right placement.
+5. **Box emission.** Emits one `LayoutBox` per input node, in input order, at the placed rectangle,
carrying the node label.
-5. **Route resolution.** Builds a `(source, target)` to polyline lookup from the engine's acyclic
+6. **Route resolution.** Builds a `(source, target)` to polyline lookup from the engine's acyclic
edge set, then emits one `LayoutLine` per input edge. `ResolveRoute` returns the forward polyline
when present, reverses the polyline of a reversed back edge, and otherwise falls back to a
straight segment between the two node centers (for a self-loop or duplicate edge the engine
@@ -50,4 +57,5 @@ the Engine subsystem. It is the entry point resolved by renderers through the la
| Rendering-Layout-LayeredAlgorithm-Identity | LayeredLayoutAlgorithm behavior described above |
| Rendering-Layout-LayeredAlgorithm-PlacesAndRoutes | LayeredLayoutAlgorithm behavior described above |
| Rendering-Layout-LayeredAlgorithm-EmptyGraph | LayeredLayoutAlgorithm behavior described above |
+| Rendering-Layout-LayeredAlgorithm-Direction | LayeredLayoutAlgorithm behavior described above |
| Rendering-Layout-LayeredAlgorithm-Validation | LayeredLayoutAlgorithm behavior described above |
diff --git a/docs/gallery/direction-down.svg b/docs/gallery/direction-down.svg
new file mode 100644
index 0000000..3a27b4b
--- /dev/null
+++ b/docs/gallery/direction-down.svg
@@ -0,0 +1,47 @@
+
diff --git a/docs/gallery/direction-right.svg b/docs/gallery/direction-right.svg
new file mode 100644
index 0000000..6ce0d03
--- /dev/null
+++ b/docs/gallery/direction-right.svg
@@ -0,0 +1,47 @@
+
diff --git a/docs/gallery/gallery.md b/docs/gallery/gallery.md
index 92faf68..febcc52 100644
--- a/docs/gallery/gallery.md
+++ b/docs/gallery/gallery.md
@@ -22,6 +22,20 @@ Sibling boxes packed compactly by the containment algorithm.
A container node holding a nested child graph, with a cross-container edge.
+## Flow direction
+
+The same directed graph laid out in two flow directions, selected with the direction option. A rightward flow arranges
+the layers left-to-right for block and pipeline diagrams; a downward flow arranges them top-to-bottom for action flows
+and state machines, swapping each node's width and height so layer spacing follows node height.
+
+
+
+The default rightward direction: layers progress left-to-right.
+
+
+
+The downward direction: the same graph's layers progress top-to-bottom.
+
## Edge routing
Orthogonal connectors step around the boxes between their endpoints instead of cutting through them.
diff --git a/docs/reqstream/rendering-layout/engine/engine.yaml b/docs/reqstream/rendering-layout/engine/engine.yaml
index b0ae6c8..49b4651 100644
--- a/docs/reqstream/rendering-layout/engine/engine.yaml
+++ b/docs/reqstream/rendering-layout/engine/engine.yaml
@@ -59,6 +59,7 @@ sections:
- Rendering-Layout-InterconnectionEngine-NonOverlapping
- Rendering-Layout-InterconnectionEngine-DummyNodes
- Rendering-Layout-InterconnectionEngine-Waypoints
+ - Rendering-Layout-InterconnectionEngine-Direction
- Rendering-Layout-InterconnectionEngine-Deterministic
tests:
- Place_LinearChain_MonotonicLayerAssignment
diff --git a/docs/reqstream/rendering-layout/engine/interconnection-layout-engine.yaml b/docs/reqstream/rendering-layout/engine/interconnection-layout-engine.yaml
index b9afd55..aa6f13e 100644
--- a/docs/reqstream/rendering-layout/engine/interconnection-layout-engine.yaml
+++ b/docs/reqstream/rendering-layout/engine/interconnection-layout-engine.yaml
@@ -52,6 +52,18 @@ sections:
- Place_SingleEdge_ProducesStraightTwoWaypointPath
- Place_LongEdge_RoutesViaDummyNodesWithinBounds
+ - id: Rendering-Layout-InterconnectionEngine-Direction
+ title: >-
+ InterconnectionLayoutEngine shall place the nodes along the requested flow direction,
+ transposing the layout and its reported total dimensions for a top-to-bottom or bottom-to-top
+ flow, and shall default to the left-to-right flow.
+ justification: |
+ Directed views request a top-to-bottom placement; the engine must honor the requested
+ direction and report total dimensions that match the transposed layout, while the default
+ leaves the established left-to-right geometry unchanged.
+ tests:
+ - Place_DownDirection_TransposesTotalsRelativeToRight
+
- id: Rendering-Layout-InterconnectionEngine-Deterministic
title: >-
Given identical input, InterconnectionLayoutEngine shall produce identical geometry and
diff --git a/docs/reqstream/rendering-layout/layered-layout-algorithm.yaml b/docs/reqstream/rendering-layout/layered-layout-algorithm.yaml
index 64869b2..5d5d360 100644
--- a/docs/reqstream/rendering-layout/layered-layout-algorithm.yaml
+++ b/docs/reqstream/rendering-layout/layered-layout-algorithm.yaml
@@ -38,6 +38,21 @@ sections:
tests:
- Apply_EmptyGraph_ReturnsEmptyCanvas
+ - id: Rendering-Layout-LayeredAlgorithm-Direction
+ title: >-
+ LayeredLayoutAlgorithm shall arrange the placed layers along the flow direction selected by
+ the direction option — top-to-bottom, bottom-to-top, right-to-left, or the default
+ left-to-right — reading the selection from the graph in preference to the options.
+ justification: |
+ Directed views such as action flows and state machines read the placed coordinates expecting
+ the layers to progress top-to-bottom; the layout is only correct for them when the algorithm
+ honors the requested flow direction rather than always flowing left-to-right.
+ tests:
+ - Apply_DownDirection_FlowsTopToBottom
+ - Apply_DownDirection_DiffersFromRight
+ - Apply_DownDirectionOnGraphScope_IsHonored
+ - Apply_DefaultDirection_FlowsLeftToRight
+
- id: Rendering-Layout-LayeredAlgorithm-Validation
title: >-
LayeredLayoutAlgorithm shall reject a null graph or null options argument with an
diff --git a/docs/reqstream/rendering-layout/rendering-layout.yaml b/docs/reqstream/rendering-layout/rendering-layout.yaml
index 5862365..ddd9f3d 100644
--- a/docs/reqstream/rendering-layout/rendering-layout.yaml
+++ b/docs/reqstream/rendering-layout/rendering-layout.yaml
@@ -30,6 +30,7 @@ sections:
- Rendering-Layout-LayeredAlgorithm-Identity
- Rendering-Layout-LayeredAlgorithm-PlacesAndRoutes
- Rendering-Layout-LayeredAlgorithm-EmptyGraph
+ - Rendering-Layout-LayeredAlgorithm-Direction
- Rendering-Layout-LayeredAlgorithm-Validation
tests:
- Apply_ChainGraph_PlacesLayeredBoxesAndRoutesEdges
diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md
index 8c56c13..90a0313 100644
--- a/docs/user_guide/introduction.md
+++ b/docs/user_guide/introduction.md
@@ -180,7 +180,7 @@ on it:
```csharp
var options = new LayoutOptions();
options.Set(CoreOptions.Algorithm, "layered");
-options.Set(CoreOptions.Direction, LayoutFlowDirection.Right);
+options.Set(CoreOptions.Direction, LayoutFlowDirection.Down); // flow the layers top-to-bottom
// Per-element overrides: any graph element is also a property holder.
a.Set(CoreOptions.NodeSpacing, 32.0);
@@ -189,6 +189,12 @@ a.Set(CoreOptions.NodeSpacing, 32.0);
options.Set(CoreOptions.EdgeRouting, EdgeRouting.Orthogonal);
```
+`CoreOptions.Direction` selects the flow direction the layered algorithm arranges its layers along:
+`Right` (the default) and `Left` flow the layers left-to-right and right-to-left, while `Down` and
+`Up` flow them top-to-bottom and bottom-to-top — the convention for action flows and state machines. A
+downward flow swaps each node's width and height before layering so layer spacing follows node height.
+As with the algorithm, a declaration on the graph takes precedence over one on the options.
+
## Routing connectors among placed boxes
When you have already positioned some boxes yourself — for example a free-form or containment layout
diff --git a/docs/verification/rendering-layout/engine/interconnection-layout-engine.md b/docs/verification/rendering-layout/engine/interconnection-layout-engine.md
index 638a73d..30b5316 100644
--- a/docs/verification/rendering-layout/engine/interconnection-layout-engine.md
+++ b/docs/verification/rendering-layout/engine/interconnection-layout-engine.md
@@ -19,6 +19,10 @@ This document maps the InterconnectionLayoutEngine unit requirements to named te
- **Waypoints** (`Rendering-Layout-InterconnectionEngine-Waypoints`):
`Place_SingleEdge_ProducesStraightTwoWaypointPath` confirms a span-one edge produces a straight
two-waypoint path; `Place_LongEdge_RoutesViaDummyNodesWithinBounds` confirms a long edge's route.
+- **Direction** (`Rendering-Layout-InterconnectionEngine-Direction`):
+ `Place_DownDirection_TransposesTotalsRelativeToRight` confirms a downward flow transposes the layout
+ relative to the default rightward flow — the same chain becomes taller than it is wide with its boxes
+ stacked in increasing Y — exercising the direction-aware total-dimension computation.
- **Deterministic** (`Rendering-Layout-InterconnectionEngine-Deterministic`): the layering scenarios
`Place_LinearChain_MonotonicLayerAssignment` and
`Place_WorkstationTopology_CorrectLayersAndNoOverlap` assert fixed, reproducible geometry for fixed
@@ -34,5 +38,7 @@ This document maps the InterconnectionLayoutEngine unit requirements to named te
Place_LongEdge_RectCountEqualsInputNodeCount, Place_LongEdge_RoutesViaDummyNodesWithinBounds
- **`Rendering-Layout-InterconnectionEngine-Waypoints`**:
Place_SingleEdge_ProducesStraightTwoWaypointPath, Place_LongEdge_RoutesViaDummyNodesWithinBounds
+- **`Rendering-Layout-InterconnectionEngine-Direction`**:
+ Place_DownDirection_TransposesTotalsRelativeToRight
- **`Rendering-Layout-InterconnectionEngine-Deterministic`**:
Place_LinearChain_MonotonicLayerAssignment, Place_WorkstationTopology_CorrectLayersAndNoOverlap
diff --git a/docs/verification/rendering-layout/layered-layout-algorithm.md b/docs/verification/rendering-layout/layered-layout-algorithm.md
index 86d94eb..c70ed44 100644
--- a/docs/verification/rendering-layout/layered-layout-algorithm.md
+++ b/docs/verification/rendering-layout/layered-layout-algorithm.md
@@ -11,6 +11,13 @@ This document maps the layered-layout-algorithm unit requirements to named test
connector per edge for a chain graph.
- **Empty graph** (`Rendering-Layout-LayeredAlgorithm-EmptyGraph`):
`Apply_EmptyGraph_ReturnsEmptyCanvas` confirms an empty graph yields an empty placed layout tree.
+- **Direction** (`Rendering-Layout-LayeredAlgorithm-Direction`):
+ `Apply_DownDirection_FlowsTopToBottom` confirms a downward flow stacks the chain in strictly
+ increasing Y on a taller-than-wide canvas; `Apply_DownDirection_DiffersFromRight` guards against the
+ option being ignored by confirming the downward layout is genuinely different from the rightward one;
+ `Apply_DownDirectionOnGraphScope_IsHonored` confirms the direction is honored when carried on the
+ graph scope; and `Apply_DefaultDirection_FlowsLeftToRight` confirms the unset default still flows
+ left-to-right so existing callers are unaffected.
- **Validation** (`Rendering-Layout-LayeredAlgorithm-Validation`): `Apply_NullGraph_Throws` confirms
a null graph argument is rejected with an argument-null error, and `Apply_NullOptions_Throws`
confirms a null options argument is likewise rejected with an argument-null error.
@@ -23,5 +30,8 @@ This document maps the layered-layout-algorithm unit requirements to named test
Apply_ChainGraph_PlacesLayeredBoxesAndRoutesEdges
- **`Rendering-Layout-LayeredAlgorithm-EmptyGraph`**:
Apply_EmptyGraph_ReturnsEmptyCanvas
+- **`Rendering-Layout-LayeredAlgorithm-Direction`**:
+ Apply_DownDirection_FlowsTopToBottom, Apply_DownDirection_DiffersFromRight,
+ Apply_DownDirectionOnGraphScope_IsHonored, Apply_DefaultDirection_FlowsLeftToRight
- **`Rendering-Layout-LayeredAlgorithm-Validation`**:
Apply_NullGraph_Throws, Apply_NullOptions_Throws
diff --git a/src/DemaConsulting.Rendering.Layout/Engine/InterconnectionLayoutEngine.cs b/src/DemaConsulting.Rendering.Layout/Engine/InterconnectionLayoutEngine.cs
index 07e16d9..5cbc92c 100644
--- a/src/DemaConsulting.Rendering.Layout/Engine/InterconnectionLayoutEngine.cs
+++ b/src/DemaConsulting.Rendering.Layout/Engine/InterconnectionLayoutEngine.cs
@@ -70,10 +70,16 @@ internal static class InterconnectionLayoutEngine
///
/// Input nodes to place, in caller order.
/// Directed edges between nodes (by index).
+ ///
+ /// The primary flow direction the layers progress along. Defaults to
+ /// , which is byte-identical to the original engine; the other
+ /// directions are realized by the pipeline's stage.
+ ///
/// Placement result with rects, layer assignments, and connector waypoints.
public static LayerResult Place(
IReadOnlyList nodes,
- IReadOnlyList edges)
+ IReadOnlyList edges,
+ LayoutDirection direction = LayoutDirection.Right)
{
ArgumentNullException.ThrowIfNull(nodes);
ArgumentNullException.ThrowIfNull(edges);
@@ -84,9 +90,9 @@ public static LayerResult Place(
return new LayerResult([], 2.0 * Padding, 2.0 * Padding, [], []);
}
- var graph = new LayeredGraph(nodes, edges, LayoutDirection.Right);
+ var graph = new LayeredGraph(nodes, edges, direction);
var pipeline = LayeredLayoutPipeline.Builder()
- .Direction(LayoutDirection.Right)
+ .Direction(direction)
.Hierarchy(Layered.HierarchyHandling.Flat)
.AddDefaultStages()
.Build();
@@ -97,21 +103,35 @@ public static LayerResult Place(
var columnX = graph.ColumnX;
var maxColWidth = graph.MaxColWidth;
- // Assemble result.
+ // Assemble result. After the pipeline's AxisTransform stage the augmented coordinates are in
+ // screen space, so each box is placed at its screen top-left with its intrinsic (un-swapped)
+ // width and height.
var rects = new Rect[n];
for (var i = 0; i < n; i++)
{
rects[i] = new Rect(augX[i], augY[i], nodes[i].Width, nodes[i].Height);
}
+ // The abstract stages compute two extents: the along-extent (layer progression, from the
+ // column geometry) and the cross-extent (within-layer, from the placed screen coordinates). A
+ // RIGHT/LEFT flow maps the along-axis to screen X and the cross-axis to screen Y; a DOWN/UP flow
+ // transposes them. Computing both extents once and assigning them by direction keeps the
+ // RIGHT path byte-identical while giving DOWN/UP correct screen dimensions.
var lastLayer = columnX.Length - 1;
- var totalWidth = columnX[lastLayer] + maxColWidth[lastLayer] + Padding;
- var totalHeight = Padding;
+ var alongTotal = columnX[lastLayer] + maxColWidth[lastLayer] + Padding;
+ var transposed = direction is LayoutDirection.Down or LayoutDirection.Up;
+ var crossTotal = Padding;
for (var i = 0; i < n; i++)
{
- totalHeight = Math.Max(totalHeight, augY[i] + nodes[i].Height + Padding);
+ var crossFar = transposed
+ ? augX[i] + nodes[i].Width
+ : augY[i] + nodes[i].Height;
+ crossTotal = Math.Max(crossTotal, crossFar + Padding);
}
+ var totalWidth = transposed ? crossTotal : alongTotal;
+ var totalHeight = transposed ? alongTotal : crossTotal;
+
return new LayerResult(rects, totalWidth, totalHeight, graph.NodeLayers, graph.Waypoints)
{
AcyclicEdges = graph.Acyclic,
diff --git a/src/DemaConsulting.Rendering.Layout/LayeredLayoutAlgorithm.cs b/src/DemaConsulting.Rendering.Layout/LayeredLayoutAlgorithm.cs
index e6eb145..7772579 100644
--- a/src/DemaConsulting.Rendering.Layout/LayeredLayoutAlgorithm.cs
+++ b/src/DemaConsulting.Rendering.Layout/LayeredLayoutAlgorithm.cs
@@ -4,6 +4,7 @@
using DemaConsulting.Rendering.Abstractions;
using DemaConsulting.Rendering.Layout.Engine;
+using DemaConsulting.Rendering.Layout.Engine.Layered;
namespace DemaConsulting.Rendering.Layout;
@@ -11,7 +12,9 @@ namespace DemaConsulting.Rendering.Layout;
/// The bundled ELK-style layered layout algorithm: arranges the input graph into Sugiyama layers and
/// routes edges orthogonally, producing a placed of boxes and connectors.
/// This is the reference implementation; it wraps the reusable layered
-/// pipeline under Engine/Layered/.
+/// pipeline under Engine/Layered/. It honors so the layers
+/// progress right, left, down, or up (a downward flow lays action-flow and state-transition diagrams
+/// out top-to-bottom).
///
public sealed class LayeredLayoutAlgorithm : ILayoutAlgorithm
{
@@ -55,7 +58,8 @@ public LayoutTree Apply(LayoutGraph graph, LayoutOptions options)
}
}
- var result = InterconnectionLayoutEngine.Place(engineNodes, engineEdges);
+ var direction = ToEngineDirection(ResolveDirection(graph, options));
+ var result = InterconnectionLayoutEngine.Place(engineNodes, engineEdges, direction);
var nodes = new List(count + graph.Edges.Count);
@@ -130,4 +134,38 @@ private static IReadOnlyList ResolveRoute(
private static Point2D Centre(Rect rect) =>
new(rect.X + (rect.Width / 2.0), rect.Y + (rect.Height / 2.0));
+
+ ///
+ /// Resolves the primary flow direction for this layout: an explicit
+ /// on the graph takes precedence, then one on the options,
+ /// falling back to the property default when neither declares one.
+ ///
+ /// The graph whose explicit direction declaration takes precedence.
+ /// The options consulted when the graph declares no direction.
+ /// The flow direction to lay the graph out along.
+ private static LayoutFlowDirection ResolveDirection(LayoutGraph graph, LayoutOptions options)
+ {
+ if (graph.TryGet(CoreOptions.Direction, out var fromGraph))
+ {
+ return fromGraph;
+ }
+
+ return options.TryGet(CoreOptions.Direction, out var fromOptions)
+ ? fromOptions
+ : CoreOptions.Direction.DefaultValue;
+ }
+
+ ///
+ /// Maps the public option to the engine's internal
+ /// the layered pipeline understands.
+ ///
+ /// The public flow direction selected through the options.
+ /// The equivalent internal engine direction.
+ private static LayoutDirection ToEngineDirection(LayoutFlowDirection direction) => direction switch
+ {
+ LayoutFlowDirection.Down => LayoutDirection.Down,
+ LayoutFlowDirection.Left => LayoutDirection.Left,
+ LayoutFlowDirection.Up => LayoutDirection.Up,
+ _ => LayoutDirection.Right,
+ };
}
diff --git a/src/DemaConsulting.Rendering/Options/CoreOptions.cs b/src/DemaConsulting.Rendering/Options/CoreOptions.cs
index 951e1fd..1d02745 100644
--- a/src/DemaConsulting.Rendering/Options/CoreOptions.cs
+++ b/src/DemaConsulting.Rendering/Options/CoreOptions.cs
@@ -54,9 +54,12 @@ public static class CoreOptions
new("rendering.hierarchyhandling", Rendering.HierarchyHandling.SeparateChildren);
///
- /// Primary flow direction for layered algorithms. Advisory in the bundled layered
- /// algorithm today, which lays out left-to-right; honoring other directions is a future,
- /// additive enhancement.
+ /// Primary flow direction for layered algorithms: the direction successive layers progress in.
+ /// Honored by the bundled layered algorithm — and
+ /// flow left-to-right and right-to-left, while
+ /// and flow
+ /// top-to-bottom and bottom-to-top (swapping each node's width and height before layering so layer
+ /// spacing is driven by node height). Defaults to .
///
public static readonly LayoutProperty Direction =
new("rendering.direction", LayoutFlowDirection.Right);
diff --git a/test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs b/test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs
index 831ef2b..cc6c58c 100644
--- a/test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs
+++ b/test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs
@@ -35,6 +35,8 @@ internal static class GalleryCatalog
public const string ContainmentPackedSvg = "containment-packed.svg";
public const string HierarchicalNestedSvg = "hierarchical-nested.svg";
public const string OrthogonalObstacleSvg = "orthogonal-obstacle.svg";
+ public const string DirectionRightSvg = "direction-right.svg";
+ public const string DirectionDownSvg = "direction-down.svg";
public const string ThemeLightPng = "theme-light.png";
public const string ThemeDarkPng = "theme-dark.png";
public const string ThemePrintPng = "theme-print.png";
@@ -63,6 +65,22 @@ internal static class GalleryCatalog
"Hierarchical nested diagram",
"A container node holding a nested child graph, with a cross-container edge."),
]),
+ new GallerySection(
+ "Flow direction",
+ "The same directed graph laid out in two flow directions, selected with the direction "
+ + "option. A rightward flow arranges the layers left-to-right for block and pipeline "
+ + "diagrams; a downward flow arranges them top-to-bottom for action flows and state "
+ + "machines, swapping each node's width and height so layer spacing follows node height.",
+ [
+ new GalleryImage(
+ DirectionRightSvg,
+ "Directed flow laid out left to right",
+ "The default rightward direction: layers progress left-to-right."),
+ new GalleryImage(
+ DirectionDownSvg,
+ "The same directed flow laid out top to bottom",
+ "The downward direction: the same graph's layers progress top-to-bottom."),
+ ]),
new GallerySection(
"Edge routing",
"Orthogonal connectors step around the boxes between their endpoints instead of cutting "
diff --git a/test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs b/test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs
index 9756006..fec7865 100644
--- a/test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs
+++ b/test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs
@@ -146,6 +146,29 @@ public static LayoutGraph ThemeShowcase()
return graph;
}
+ ///
+ /// A compact directed flow — a start, two sequential steps, a branch, and an end — used to show
+ /// the same graph laid out in two flow directions. Read left-to-right it is a pipeline; read
+ /// top-to-bottom it is an action flow, which is exactly what the direction option selects.
+ ///
+ /// A flat directed graph suited to a direction comparison.
+ public static LayoutGraph DirectionShowcase()
+ {
+ var graph = new LayoutGraph();
+ var start = AddLabelled(graph, "start", "Start");
+ var collect = AddLabelled(graph, "collect", "Collect");
+ var analyze = AddLabelled(graph, "analyze", "Analyze");
+ var report = AddLabelled(graph, "report", "Report");
+ var archive = AddLabelled(graph, "archive", "Archive");
+
+ Connect(graph, "start-collect", start, collect);
+ Connect(graph, "collect-analyze", collect, analyze);
+ Connect(graph, "analyze-report", analyze, report);
+ Connect(graph, "analyze-archive", analyze, archive);
+
+ return graph;
+ }
+
/// Adds a labelled leaf node of the standard showcase size to the given graph.
private static LayoutGraphNode AddLabelled(LayoutGraph graph, string id, string label)
{
diff --git a/test/DemaConsulting.Rendering.Gallery/GalleryShowcaseTests.cs b/test/DemaConsulting.Rendering.Gallery/GalleryShowcaseTests.cs
index b07c39b..6ec4d1d 100644
--- a/test/DemaConsulting.Rendering.Gallery/GalleryShowcaseTests.cs
+++ b/test/DemaConsulting.Rendering.Gallery/GalleryShowcaseTests.cs
@@ -71,6 +71,40 @@ public void Gallery_OrthogonalObstacle_RendersSvg()
Themes.Dark);
}
+ ///
+ /// Renders the direction showcase left-to-right, proving the layered algorithm honors the
+ /// default rightward flow direction.
+ ///
+ [Fact]
+ public void Gallery_DirectionRight_RendersSvg()
+ {
+ var options = LayoutOptions.ForAlgorithm("layered");
+ options.Set(CoreOptions.Direction, LayoutFlowDirection.Right);
+
+ GalleryWriter.Svg(
+ GalleryCatalog.DirectionRightSvg,
+ GalleryDiagrams.DirectionShowcase(),
+ options,
+ Themes.Dark);
+ }
+
+ ///
+ /// Renders the same direction showcase top-to-bottom, proving the layered algorithm honors a
+ /// downward flow direction and produces a genuinely different, transposed layout.
+ ///
+ [Fact]
+ public void Gallery_DirectionDown_RendersSvg()
+ {
+ var options = LayoutOptions.ForAlgorithm("layered");
+ options.Set(CoreOptions.Direction, LayoutFlowDirection.Down);
+
+ GalleryWriter.Svg(
+ GalleryCatalog.DirectionDownSvg,
+ GalleryDiagrams.DirectionShowcase(),
+ options,
+ Themes.Dark);
+ }
+
///
/// Renders the representative diagram with the light theme to PNG, giving it a solid light
/// background.
diff --git a/test/DemaConsulting.Rendering.Layout.Tests/Engine/InterconnectionLayoutEngineTests.cs b/test/DemaConsulting.Rendering.Layout.Tests/Engine/InterconnectionLayoutEngineTests.cs
index c3f5eb5..6980596 100644
--- a/test/DemaConsulting.Rendering.Layout.Tests/Engine/InterconnectionLayoutEngineTests.cs
+++ b/test/DemaConsulting.Rendering.Layout.Tests/Engine/InterconnectionLayoutEngineTests.cs
@@ -3,6 +3,7 @@
//
using DemaConsulting.Rendering.Layout.Engine;
+using DemaConsulting.Rendering.Layout.Engine.Layered;
namespace DemaConsulting.Rendering.Layout.Tests.Engine;
@@ -164,6 +165,30 @@ public void Place_WorkstationTopology_CorrectLayersAndNoOverlap()
}
}
+ ///
+ /// A downward flow direction transposes the layout: the same chain that lays out wider than it
+ /// is tall in the default rightward direction lays out taller than it is wide when placed
+ /// downward, with its boxes stacked in strictly increasing Y. This exercises the direction-aware
+ /// total-dimension computation.
+ ///
+ [Fact]
+ public void Place_DownDirection_TransposesTotalsRelativeToRight()
+ {
+ var nodes = new List { new(80, 40), new(80, 40), new(80, 40) };
+ var edges = new List { new(0, 1), new(1, 2) };
+
+ var right = InterconnectionLayoutEngine.Place(nodes, edges, LayoutDirection.Right);
+ var down = InterconnectionLayoutEngine.Place(nodes, edges, LayoutDirection.Down);
+
+ // Rightward flow is wider than tall; the downward flow transposes those dimensions.
+ Assert.True(right.TotalWidth > right.TotalHeight);
+ Assert.True(down.TotalHeight > down.TotalWidth);
+
+ // The downward flow stacks the chain's boxes in strictly increasing Y.
+ Assert.True(down.Rects[0].Y < down.Rects[1].Y);
+ Assert.True(down.Rects[1].Y < down.Rects[2].Y);
+ }
+
private static bool Overlaps(Rect a, Rect b) =>
a.X < b.X + b.Width &&
b.X < a.X + a.Width &&
diff --git a/test/DemaConsulting.Rendering.Layout.Tests/LayeredLayoutAlgorithmTests.cs b/test/DemaConsulting.Rendering.Layout.Tests/LayeredLayoutAlgorithmTests.cs
index ed9eb26..9531b0d 100644
--- a/test/DemaConsulting.Rendering.Layout.Tests/LayeredLayoutAlgorithmTests.cs
+++ b/test/DemaConsulting.Rendering.Layout.Tests/LayeredLayoutAlgorithmTests.cs
@@ -76,6 +76,115 @@ public void Apply_NullGraph_Throws()
() => new LayeredLayoutAlgorithm().Apply(null!, new LayoutOptions()));
}
+ ///
+ /// Proves that a null options argument is rejected.
+ ///
+ [Fact]
+ public void Apply_NullOptions_Throws()
+ {
+ Assert.Throws(
+ () => new LayeredLayoutAlgorithm().Apply(new LayoutGraph(), null!));
+ }
+
+ ///
+ /// Proves that selecting a downward flow direction lays the chain out top-to-bottom: boxes are
+ /// stacked in strictly increasing Y (rather than the default left-to-right increasing X), and
+ /// the canvas is taller than it is wide.
+ ///
+ [Fact]
+ public void Apply_DownDirection_FlowsTopToBottom()
+ {
+ var graph = new LayoutGraph();
+ var a = graph.AddNode("a", 80, 40);
+ var b = graph.AddNode("b", 80, 40);
+ var c = graph.AddNode("c", 80, 40);
+ graph.AddEdge("e1", a, b);
+ graph.AddEdge("e2", b, c);
+
+ var options = new LayoutOptions();
+ options.Set(CoreOptions.Direction, LayoutFlowDirection.Down);
+
+ var tree = new LayeredLayoutAlgorithm().Apply(graph, options);
+
+ var boxes = tree.Nodes.OfType().ToList();
+ Assert.Equal(3, boxes.Count);
+
+ // A downward chain stacks its boxes vertically: strictly increasing Y.
+ Assert.True(boxes[0].Y < boxes[1].Y);
+ Assert.True(boxes[1].Y < boxes[2].Y);
+
+ // A three-deep top-to-bottom flow of short boxes is taller than it is wide.
+ Assert.True(tree.Height > tree.Width);
+ }
+
+ ///
+ /// Proves that the downward flow is a genuinely different layout from the default rightward
+ /// flow — a regression guard against the option being silently ignored (which would return the
+ /// identical left-to-right coordinates for both directions).
+ ///
+ [Fact]
+ public void Apply_DownDirection_DiffersFromRight()
+ {
+ var algorithm = new LayeredLayoutAlgorithm();
+
+ var right = algorithm.Apply(BuildChain(), new LayoutOptions());
+
+ var downOptions = new LayoutOptions();
+ downOptions.Set(CoreOptions.Direction, LayoutFlowDirection.Down);
+ var down = algorithm.Apply(BuildChain(), downOptions);
+
+ // Rightward is wide-and-short; downward is tall-and-narrow. They must not be identical.
+ Assert.True(right.Width > right.Height);
+ Assert.True(down.Height > down.Width);
+
+ var rightBoxes = right.Nodes.OfType().ToList();
+ var downBoxes = down.Nodes.OfType().ToList();
+ Assert.True(rightBoxes[0].X < rightBoxes[2].X);
+ Assert.True(downBoxes[0].Y < downBoxes[2].Y);
+ }
+
+ ///
+ /// Proves that the flow direction is honored when carried on the graph scope, mirroring how the
+ /// algorithm resolves its other well-known options (graph scope takes precedence over options).
+ ///
+ [Fact]
+ public void Apply_DownDirectionOnGraphScope_IsHonored()
+ {
+ var graph = BuildChain();
+ graph.Set(CoreOptions.Direction, LayoutFlowDirection.Down);
+
+ var tree = new LayeredLayoutAlgorithm().Apply(graph, new LayoutOptions());
+
+ Assert.True(tree.Height > tree.Width);
+ }
+
+ ///
+ /// Proves that the default (unset) direction lays the graph out left-to-right, so existing
+ /// callers that never set the option are unaffected.
+ ///
+ [Fact]
+ public void Apply_DefaultDirection_FlowsLeftToRight()
+ {
+ var tree = new LayeredLayoutAlgorithm().Apply(BuildChain(), new LayoutOptions());
+
+ var boxes = tree.Nodes.OfType().ToList();
+ Assert.True(boxes[0].X < boxes[1].X);
+ Assert.True(boxes[1].X < boxes[2].X);
+ Assert.True(tree.Width > tree.Height);
+ }
+
+ /// Builds the standard three-node chain graph used by the direction tests.
+ private static LayoutGraph BuildChain()
+ {
+ var graph = new LayoutGraph();
+ var a = graph.AddNode("a", 80, 40);
+ var b = graph.AddNode("b", 80, 40);
+ var c = graph.AddNode("c", 80, 40);
+ graph.AddEdge("e1", a, b);
+ graph.AddEdge("e2", b, c);
+ return graph;
+ }
+
///
/// Proves that the hierarchical input-model capability is behavior-preserving: a flat graph
/// whose nodes declare no children lays out exactly as before, because the layered algorithm