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
15 changes: 15 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ downward/upward flow). The gallery therefore ships two companion diagrams,
`ContentInsetLeft` case) and `ports-showcase-vertical` (downward flow; top/bottom ports), which
together exercise all four sides.

**`HierarchicalLayoutAlgorithm` bug fix (same-scope port edges, distinct from Phase 2 below).**
`HierarchicalLayoutAlgorithm` (the compound/recursive engine, not the flat `layered` algorithm
itself) previously dropped *every* edge touching a named `LayoutGraphPort` the instant its scope
contained any container node at all, even when the port edge's own endpoints were both literal,
non-nested members of that scope (for example two root-level siblings, with some unrelated
container elsewhere in the same scope) — silently, with no diagnostic. This has been fixed: a
same-scope port edge now reaches the selected leaf algorithm and is routed exactly as it would be in
a flat (container-free) graph. A port edge that genuinely crosses a container boundary — one
endpoint's owning node nested inside a container relative to the scope while the other is not, or an
edge otherwise sharing a box with such an edge — still has no anchoring/routing design and now fails
loudly with `NotSupportedException` instead of being silently dropped, pending the
`HierarchyHandling.Recursive`/boundary-port work described below and in "Phase 2 (hierarchy, once
`HierarchyHandling.Recursive` exists)" further down this file. Implementing actual
boundary-crossing port routing/anchoring remains out of scope for this fix.

The bundled `layered` algorithm's layout pipeline currently treats the input graph as simple:
when two or more edges share the same directed `(source, target)` node pair, only one is
retained and routed, and every input edge sharing that pair resolves to the same single routed
Expand Down
41 changes: 29 additions & 12 deletions docs/design/rendering-layout/hierarchical-layout-algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,36 @@ overrides win over the supplied options, exactly like every nested scope), and c
records both the sub-layout and the container's effective size — the sub-layout size grown by
`ContainerPadding` on every side plus a title band when the container is labelled.
3. **Sized view.** The engine builds an internal, side-effect-free *view* graph with the same nodes in
the same order (container nodes carrying their effective size, leaves their own size, labels copied),
only the edges whose endpoints are both direct members of this scope. Every `CoreOptions` property
this scope needs is already present in `effective`, so the view carries no options of its own — the
leaf algorithm resolves them from `effective`, not from the view graph. The caller's input graph is
never mutated.
the same order (container nodes carrying their effective size, leaves their own size, labels copied,
and each direct member's named ports copied onto its view counterpart), only the edges whose direct-
member endpoints are both under this scope and neither nested inside a container relative to it —
including an edge whose endpoint is a named `LayoutGraphPort` rather than the node itself, so a
same-scope port edge is routed by the leaf algorithm exactly as it would be in a flat (container-free)
graph, regardless of whether the scope also contains an unrelated container elsewhere. Every
`CoreOptions` property this scope needs is already present in `effective`, so the view carries no
options of its own — the leaf algorithm resolves them from `effective`, not from the view graph. The
caller's input graph is never mutated.
4. **Placement.** The resolved leaf algorithm places the sized view against `effective`, emitting one box
per node in input order followed by routed lines for the in-scope edges.
per node in input order, followed by routed lines and any named-port anchors (`LayoutPort`) for the
in-scope edges.
5. **Composition.** Each container's placed box receives its recursively laid-out children, translated
from their local origin to the box's padded (and title-offset) interior via a recursive `Translate`
that shifts nested boxes and line waypoints (local-to-absolute translation, following the
`ComponentPacker` precedent).
that shifts nested boxes, line waypoints, and port anchors (local-to-absolute translation, following
the `ComponentPacker` precedent).
6. **Cross-container (LCA) routing.** Edges whose endpoints resolve to different direct-member
containers of this scope — mapped from any descendant endpoint up to its owning top-level box — are
routed at this level with `ConnectorRouter.Route`, steering around the sibling boxes; the
`EdgeRouting` style is read from this scope's own cascaded `effective` snapshot, so an override set
on the owning scope's graph is honored rather than falling back to the root options. Edges already
routed by the leaf algorithm (both endpoints direct) or belonging to a lower scope (both endpoints
under one container) are skipped.
under one container) are skipped. A genuine cross-container edge whose endpoint is a named
`LayoutGraphPort` — this scope's router is built on the box-only `ConnectorRouter`/`Connection` and
has no port concept — throws `NotSupportedException` instead of being routed or silently dropped;
full boundary-crossing port support (anchoring, routing) is a separate, not-yet-designed Phase 2
effort (see ROADMAP.md).
7. **Assembly.** The engine returns a `LayoutTree` with the leaf algorithm's canvas size for this level
and the composed boxes followed by the leaf-routed lines and the cross-container lines.
and the composed boxes followed by the leaf-routed lines, any leaf-emitted port anchors, and the
cross-container lines.

Every `CoreOptions` property (`Algorithm`, `Direction`, `EdgeRouting`, `HierarchyHandling`,
`NodeSpacing`, `LayerSpacing`) cascades through this same generalized mechanism, built once on
Expand Down Expand Up @@ -99,8 +109,15 @@ input graph and ignores the supplied options would silently break cascading for

Null `graph`, `options`, or (injecting constructor) `registry` throw `ArgumentNullException`. A scope
that selects an algorithm identifier absent from the registry surfaces the registry's
`KeyNotFoundException`. Edges whose endpoints are not under the current scope are skipped rather than
treated as errors.
`KeyNotFoundException`. An edge whose endpoint resolves to no node at all under the current scope (an
out-of-scope reference) is skipped rather than treated as an error. A port edge that genuinely crosses
a container boundary — one endpoint's owning node nested inside a container relative to this scope
while the other is not, or an edge otherwise promoted into this scope's router because it shares a box
with such an edge — throws `NotSupportedException` with a message identifying named ports crossing a
container boundary as not yet supported; this scope's router has no port concept and full
boundary-crossing port support remains a separate Phase 2 effort. A same-scope port edge (neither
endpoint nested relative to this scope) is not an error case: it is routed locally by the leaf
algorithm exactly as it would be in a flat graph.

### HierarchicalLayoutAlgorithm Dependencies

Expand Down
30 changes: 30 additions & 0 deletions docs/reqstream/rendering-layout/hierarchical-layout-algorithm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@ sections:
tests:
- Apply_CrossContainerEdge_HonorsScopeEdgeRoutingOverride

- id: Rendering-Layout-HierarchicalLayout-SameScopePortEdges
title: >-
HierarchicalLayoutAlgorithm shall route a same-scope edge whose endpoint is a named
LayoutGraphPort through the selected leaf algorithm exactly as a flat (container-free) graph
would, even when the scope also contains an unrelated container node.
justification: |
Previously ClassifyEdges unconditionally skipped (silently dropped) any edge touching a
LayoutGraphPort the instant the scope contained any container node at all — a
previously-undocumented defect, now fixed — regardless of whether the port edge actually
crossed a container boundary. A port edge whose direct-member endpoints are both literal,
non-nested members of the scope does not cross any boundary and must reach the leaf algorithm,
which already resolves a LayoutGraphPort endpoint correctly.
tests:
- Apply_SameScopePortEdge_WithUnrelatedContainerElsewhere_RoutesLikePortEdge
- Apply_NestedContainerPortEdge_TranslatesPortIntoAncestorCoordinates

- id: Rendering-Layout-HierarchicalLayout-BoundaryPortEdgeThrows
title: >-
HierarchicalLayoutAlgorithm shall throw NotSupportedException, rather than silently dropping
the edge, when a named-port edge genuinely crosses a container boundary.
justification: |
This scope's cross-container router is built on the box-only ConnectorRouter/Connection and
has no port concept, so it cannot anchor or route a port endpoint. Failing loudly with a clear,
actionable message is safer than silently dropping the edge (the previous, previously-
undocumented defect behavior) or anchoring it incorrectly as a plain box connector. Full
boundary-crossing port support (anchoring, routing) remains a separate, not-yet-designed Phase
2 effort (see ROADMAP.md).
tests:
- Apply_PortEdgeCrossingContainerBoundary_Throws

- id: Rendering-Layout-HierarchicalLayout-ValidatesGraph
title: >-
HierarchicalLayoutAlgorithm shall reject a null graph argument with an argument-null error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ A verification run passes when every named scenario below asserts without unexpe
the referenced tests cover each `Rendering-Layout-HierarchicalLayout-*` requirement. Any drift in
the stable identifier (`"hierarchical"`), in the flat-graph byte-equivalence guarantee, in
container sizing (padding, title band), in per-scope algorithm resolution, in cross-container
LCA edge routing, or in the argument-null validation behavior constitutes a failure. Mutation of
input node sizes also constitutes a failure.
LCA edge routing, in same-scope named-port edge routing (or its ancestor-coordinate translation), in
the boundary-crossing port `NotSupportedException` behavior, or in the argument-null validation
behavior constitutes a failure. Mutation of input node sizes also constitutes a failure.

### Test Scenarios

Expand Down Expand Up @@ -80,6 +81,20 @@ input node sizes also constitutes a failure.
`Apply_CrossContainerEdge_HonorsScopeEdgeRoutingOverride` confirms a cross-container edge is routed
using the owning scope's own cascaded `CoreOptions.EdgeRouting` override rather than the root
options.
- **Same-scope port edges** (`Rendering-Layout-HierarchicalLayout-SameScopePortEdges`):
`Apply_SameScopePortEdge_WithUnrelatedContainerElsewhere_RoutesLikePortEdge` confirms a port-to-node
edge between two root-level siblings is routed by the leaf algorithm — emitting exactly one
`LayoutPort` carrying the port's external label, anchored on the source box's boundary, and exactly
one connecting `LayoutLine` — even though the scope also contains an unrelated container node
elsewhere with no edges of its own.
`Apply_NestedContainerPortEdge_TranslatesPortIntoAncestorCoordinates` confirms a `LayoutPort` emitted
by a nested container's own leaf pass is correctly translated into the ancestor's absolute
coordinates when composed, landing within the composed container box's bounds.
- **Boundary port edge throws** (`Rendering-Layout-HierarchicalLayout-BoundaryPortEdgeThrows`):
`Apply_PortEdgeCrossingContainerBoundary_Throws` confirms an edge from a root-level named port
directly to a node nested inside a separate container — a genuine boundary-crossing port edge —
throws `NotSupportedException` with a message identifying named ports crossing a container boundary
as not yet supported, rather than the edge being silently dropped.
- **Validation** (`Rendering-Layout-HierarchicalLayout-ValidatesGraph`,
`Rendering-Layout-HierarchicalLayout-ValidatesOptions`,
`Rendering-Layout-HierarchicalLayout-ValidatesRegistry`): `Apply_NullGraph_Throws`,
Expand Down Expand Up @@ -107,6 +122,11 @@ input node sizes also constitutes a failure.
Apply_ThreeLevelEdgeRoutingCascade_ReachesEveryLeafAlgorithmCall
- **`Rendering-Layout-HierarchicalLayout-HonorsScopeEdgeRouting`**:
Apply_CrossContainerEdge_HonorsScopeEdgeRoutingOverride
- **`Rendering-Layout-HierarchicalLayout-SameScopePortEdges`**:
Apply_SameScopePortEdge_WithUnrelatedContainerElsewhere_RoutesLikePortEdge,
Apply_NestedContainerPortEdge_TranslatesPortIntoAncestorCoordinates
- **`Rendering-Layout-HierarchicalLayout-BoundaryPortEdgeThrows`**:
Apply_PortEdgeCrossingContainerBoundary_Throws
- **`Rendering-Layout-HierarchicalLayout-ValidatesGraph`**:
Apply_NullGraph_Throws
- **`Rendering-Layout-HierarchicalLayout-ValidatesOptions`**:
Expand Down
Loading
Loading