Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/gallery/nested-hierarchy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ independently, and packs the results into one combined canvas.
"auto" routes any component containing a container node through the hierarchical algorithm regardless of its size, while
the unrelated isolated sibling is packed alongside it through the shared containment bucket.

![Three-level nested container mixing connected pairs and singletons](auto-deep-nested-mixed-connectivity.svg)

Every nested scope inherits "auto" without re-declaring it, and each one is independently re-classified there: the
connected pair routes through layered and the singleton is packed alongside it through containment, at every level of
nesting — not just the root.

## Boundary and delegation ports

The hierarchical engine's support for boundary (delegation) ports: a container may expose a named port carrying BOTH an
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions docs/gallery/nested-hierarchy/auto-nested-routes-hierarchical.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 37 additions & 14 deletions src/DemaConsulting.Rendering.Layout/AutoLayoutAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,40 @@
/// </summary>
private const double ComponentAspectRatio = 4.0 / 3.0;

private readonly HierarchicalLayoutAlgorithm _hierarchical = new();
private readonly HierarchicalLayoutAlgorithm _hierarchical;
private readonly LayeredLayoutAlgorithm _layered = new();
private readonly ContainmentLayoutAlgorithm _containment = new();

/// <summary>
/// Initializes a new instance of the <see cref="AutoLayoutAlgorithm"/> class.
/// </summary>
/// <remarks>
/// The internal <see cref="HierarchicalLayoutAlgorithm"/> instance used to recurse into
/// container-routed groups is built with a registry that includes this engine itself under its
/// own <see cref="AlgorithmId"/>, in addition to the bundled leaf algorithms. This is what makes
/// "auto" behave as documented for <see cref="CoreOptions.Algorithm"/>'s inheritance rules: a
/// nested container scope that does not re-declare its own algorithm inherits "auto" like any
/// other cascaded option value, and — because the recursion registry can resolve "auto" back to
/// this same instance — that scope is re-classified by this algorithm's own connectivity-based
/// routing, exactly as if a caller had selected "auto" for it directly. "Auto" is therefore
/// re-evaluated at every scope it cascades to, never resolved once and locked in as a fixed
/// concrete choice for descendant scopes. A raw <see cref="HierarchicalLayoutAlgorithm"/>
/// constructed independently of this class still has zero knowledge of "auto" and continues to
/// surface a resolution error for it, preserving that engine's documented independence.
/// </remarks>
public AutoLayoutAlgorithm()
{
_hierarchical = new HierarchicalLayoutAlgorithm(new LayoutAlgorithmRegistry()
.Register(_layered)
.Register(_containment)
.Register(this));
}

/// <inheritdoc/>
public override string Id => AlgorithmId;

/// <inheritdoc/>
protected internal override LayoutTree ApplyCore(LayoutGraph graph, LayoutOptions options)

Check warning on line 160 in src/DemaConsulting.Rendering.Layout/AutoLayoutAlgorithm.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Refactor this method to reduce its Cognitive Complexity from 34 to the 15 allowed.

Check warning on line 160 in src/DemaConsulting.Rendering.Layout/AutoLayoutAlgorithm.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Refactor this method to reduce its Cognitive Complexity from 34 to the 15 allowed.
{
ArgumentNullException.ThrowIfNull(graph);
ArgumentNullException.ThrowIfNull(options);
Expand Down Expand Up @@ -244,7 +269,12 @@
}

// Fast path: exactly one group overall means nothing needs to be split — delegate straight to
// that group's algorithm on the original, unmodified graph.
// that group's algorithm on the original, unmodified graph. This is safe for every routed
// algorithm, including HierarchicalLayoutAlgorithm: when this graph's own cascaded options
// resolve CoreOptions.Algorithm to "auto" (the normal way a caller selects this engine), that
// same "auto" value survives into HierarchicalLayoutAlgorithm's own top-scope resolution, but
// its recursion registry (built in this class's constructor) resolves "auto" back to this same
// instance rather than throwing — re-running this algorithm's own classification for that scope.
if (routedGroups.Count == 1 && singletons.Count == 0)
{
return routedGroups[0].Algorithm.ApplyCore(graph, options);
Expand All @@ -256,19 +286,12 @@
}

// Genuine multi-group case: capture the graph's own cascaded options once (so a graph-level
// override still applies to every split-off piece), split each group into its own freshly-built
// sub-graph, lay each out independently, and pack the results into one combined tree.
//
// The captured options must not keep carrying this graph's own CoreOptions.Algorithm value
// (typically "auto" itself, since that is how a caller selected this algorithm in the first
// place): each split-off group's leaf/hierarchical algorithm was already chosen by the routing
// rule above, but HierarchicalLayoutAlgorithm re-reads CoreOptions.Algorithm from its own
// effective options to resolve ITS OWN top scope's leaf algorithm, and "auto" is never a
// registered leaf identifier there. Resetting it to the layered default (the same default
// HierarchicalLayoutAlgorithm itself falls back to when nothing declares an override) restores
// the cascade to exactly what an ordinary caller not using "auto" would see.
// override, including "auto" itself, still applies to every split-off piece), split each group
// into its own freshly-built sub-graph, lay each out independently, and pack the results into one
// combined tree. A split-off piece routed to HierarchicalLayoutAlgorithm resolves "auto" the same
// way the fast path above does — back to this instance, via the recursion registry — so nested
// container scopes are re-classified rather than defaulting to a fixed leaf choice.
var effective = graph.OverlayOnto(options);
effective.Set(CoreOptions.Algorithm, LayeredLayoutAlgorithm.AlgorithmId);

var trees = new List<LayoutTree>(routedGroups.Count + (singletons.Count > 0 ? 1 : 0));
foreach (var (members, algorithm) in routedGroups)
Expand Down
9 changes: 9 additions & 0 deletions test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ internal static class GalleryCatalog
public const string HierarchicalNestedPng = "nested-hierarchy/hierarchical-nested.png";
public const string AutoNestedRoutesHierarchicalSvg =
"nested-hierarchy/auto-nested-routes-hierarchical.svg";
public const string AutoDeepNestedMixedConnectivitySvg =
"nested-hierarchy/auto-deep-nested-mixed-connectivity.svg";
public const string BoundaryPortsShowcaseHorizontalSvg =
"nested-hierarchy/boundary-ports-showcase-horizontal.svg";
public const string BoundaryPortsShowcaseHorizontalPng =
Expand Down Expand Up @@ -353,6 +355,13 @@ internal static class GalleryCatalog
+ "hierarchical algorithm regardless of its size, while the unrelated "
+ "isolated sibling is packed alongside it through the shared containment "
+ "bucket."),
new GalleryImage(
AutoDeepNestedMixedConnectivitySvg,
"Three-level nested container mixing connected pairs and singletons",
"Every nested scope inherits \"auto\" without re-declaring it, and each one "
+ "is independently re-classified there: the connected pair routes through "
+ "layered and the singleton is packed alongside it through containment, at "
+ "every level of nesting — not just the root."),
]),
new GallerySection(
"Boundary and delegation ports",
Expand Down
40 changes: 40 additions & 0 deletions test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,46 @@ public static LayoutGraph AutoNestedRoutesHierarchical()
return graph;
}

/// <summary>
/// A three-level-deep compound container whose intermediate and innermost scopes each mix a
/// connected pair with an unrelated singleton, none of them re-declaring
/// <see cref="CoreOptions.Algorithm"/>. Every nested scope inherits <c>"auto"</c> from the root
/// graph and must be independently re-classified there — proving <c>"auto"</c> is re-evaluated
/// at each level it cascades to (routing the connected pair through layered and the singleton
/// through containment at every scope), rather than being resolved once at the root and then
/// applied uniformly to every descendant scope.
/// </summary>
/// <returns>A graph with a two-level nested container plus an unrelated root-level singleton.</returns>
public static LayoutGraph AutoDeepNestedMixedConnectivity()
{
var graph = new LayoutGraph();

// Level 1: the outer container.
var outer = graph.AddNode("outer", 10, 10);
outer.Label = "Outer";

// Level 2 (inside outer.Children): a connected pair, an unrelated singleton, and another
// container going one level deeper still.
var mid1 = AddLabelled(outer.Children, "mid1", "Mid1");
var mid2 = AddLabelled(outer.Children, "mid2", "Mid2");
Connect(outer.Children, "mid1-mid2", mid1, mid2);
AddLabelled(outer.Children, "midSolo", "MidSolo");

var deepContainer = outer.Children.AddNode("deepContainer", 10, 10);
deepContainer.Label = "DeepContainer";

// Level 3 (inside deepContainer.Children): again a connected pair plus an unrelated singleton.
var d1 = AddLabelled(deepContainer.Children, "d1", "D1");
var d2 = AddLabelled(deepContainer.Children, "d2", "D2");
Connect(deepContainer.Children, "d1-d2", d1, d2);
AddLabelled(deepContainer.Children, "dSolo", "DSolo");

// An unrelated root-level singleton forces the genuine multi-group split path at the root too.
AddLabelled(graph, "rootSolo", "RootSolo");

return graph;
}

/// <summary>
/// Twelve small, wide sibling boxes with no edges, suited to the containment algorithm's
/// column-count-based content-width candidate: without it, the area-based width estimate alone
Expand Down
7 changes: 5 additions & 2 deletions test/DemaConsulting.Rendering.Gallery/GalleryIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ public static string BuildTopIndex()
+ "each one demonstrates and, where relevant, which bug it guards against. This page just "
+ "points the way and shows a taste of each.");

AppendParagraph(builder, "| Group | What it's about |");
AppendParagraph(builder, "| --- | --- |");
// Written directly (not via AppendParagraph) so the header and separator rows are not
// blank-line-separated from each other or from the data rows: a Markdown table is one
// contiguous block, and a blank line between its rows would break it into separate tables.
builder.Append("| Group | What it's about |").Append('\n');
builder.Append("| --- | --- |").Append('\n');
foreach (var group in GalleryCatalog.Groups)
{
builder
Expand Down
Loading
Loading