Skip to content

Enhance boundary port handling and layout pipeline features#14

Merged
Malcolmnixon merged 23 commits into
mainfrom
feature/hierarchical-boundary-ports
Jul 9, 2026
Merged

Enhance boundary port handling and layout pipeline features#14
Malcolmnixon merged 23 commits into
mainfrom
feature/hierarchical-boundary-ports

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

This pull request significantly expands the documentation for the hierarchical layout engine, focusing on its new support for recursive, compound-graph layout and especially the handling of boundary (delegation) ports. The changes clarify how the engine now detects, lays out, and resolves boundary ports in a single recursive pass, ensuring orthogonal edge routing and correct anchor sharing between external and internal connections. The documentation also details the new two-pass container sizing process, improved tie-breaking for connector routing, and updates to error handling and dependencies.

Hierarchical/Recursive Layout and Boundary Port Support

  • The hierarchical layout algorithm now supports a recursive, ELK-style combined pass for compound graphs, enabling containers and their nested descendants to be laid out together in one pass. This includes detection and resolution of boundary (delegation) ports, which are now anchored correctly and routed orthogonally for both external and internal edges. [1] [2] [3] [4]
  • The documentation describes how boundary ports are detected structurally, how the recursive pipeline is assembled and run (RunRecursive), and how the placed result is projected back into per-scope geometry, with all converging edges routed directly to the shared anchor. [1] [2]

Algorithm and Pipeline Design Updates

  • The layered pipeline builder now supports both flat and recursive hierarchy handling, with new methods and a revised assembly process. The recursive path is no longer a placeholder and is fully runnable, with specific handling for recursive layer assignment and crossing minimization.
  • The recursive pass includes two-pass cascading container sizing, ensuring that containers grow as needed to fit their contents, with repeated passes until all sizes settle or a maximum is reached.

Routing and Tie-Break Improvements

  • The connector router's tie-breaking logic for slot assignment now avoids arbitrarily separating segments that converge or diverge at the same anchor, producing more symmetric and predictable layouts for fan-in and fan-out at boundary ports.

Error Handling and API Contracts

  • Error handling is clarified: only ports that cannot be handled by the delegation mechanism (e.g., ports on plain nodes with edges into other containers) throw NotSupportedException; boundary ports on containers are now fully supported. [1] [2]
  • The documentation updates the dependencies and public API contracts to reflect the new recursive and boundary-port-related helpers and algorithms. [1] [2] [3]

Other Documentation and Spelling

  • Added new terms related to layout and geometry to the spell-checker configuration.
  • Added a "Features" section to the README.md highlighting the pluggable algorithms, orthogonal routing, named ports, boundary ports, and multiple output formats.

Malcolm Nixon added 23 commits July 8, 2026 19:53
…ers place outward external label on boundary ports (Stage 1)
Extend AugNode with an optional HierarchyCrossing descriptor (originating LayoutGraphPort + External/Internal face) keeping default construction byte-identical for existing callers. Enable the LayeredLayoutPipeline Recursive path by removing the NotSupportedException throw and adding AddRecursiveStages(); the Flat stage list is unchanged.
HierarchyMergeRegionBuilder detects boundary (delegation) ports structurally per scope and transitively across a whole hierarchy. BoundaryPortResolver reconciles each boundary port into one shared anchor carrying both labels, consolidates external fan-out, and wires one internal delegation connector per edge, ordering same-face crossings via the recursive layered pipeline.
LayoutScope now collects boundary ports and, when at least one is present, reconciles the leaf pass's external anchor with the container's placed interior via BoundaryPortResolver, emitting one shared anchor carrying both labels plus internal delegation connectors. Gated strictly behind boundary-port detection so boundary-port-free scopes stay byte-identical. Adds ToEngineDirection helper.
Repurpose the boundary-crossing port test into the positive Stage 1 acceptance assertion (one shared anchor carrying both labels, both connectors reaching it); add fan-out and two-independent-ports integration tests; keep a narrower _Throws for the genuinely unsupported non-container-port-into-different-container shape. Add HierarchyMergeRegionBuilder and BoundaryPortResolver unit tests including a three-level-chain and two-independent-ports case. Update the pipeline recursive-hierarchy test to assert the now-supported behavior.
…lding docs honest

Address quality-review MEDIUM concerns:
- Add SVG and Skia renderer tests exercising a boundary port with both
  ExternalLabel and InternalLabel (internal inward, external outward about the
  shared anchor) plus external-label-only legacy-parity assertions.
- Correct XML docs that overstated the hierarchy-crossing dummy: the
  descriptor/OrderCrossings primitive are unit-tested and reserved for the
  future fully-joint pass; production placement is reconciliation-based and
  does not populate AugNode.Crossing. Fix the misleading discarded-result
  comment in SynthesizeAnchor accordingly.
…tity, not ExternalLabel

BoundaryPortResolver previously matched a boundary port to its leaf-placed anchor
(FindLeafAnchors) and matched nested delegation-chain targets
(ResolveInteriorTargets) by string.Equals on the optional, frequently-null
LayoutGraphPort.ExternalLabel. Two independent boundary ports on the same
container that shared (or both left null) their ExternalLabel were silently
mis-reconciled: one anchor absorbed both ports' external connectors while the
other anchor was left with none.

Fix: thread a stable identity channel end-to-end.
- LayoutPort gains an optional trailing SourcePort (LayoutGraphPort?) field:
  engine-only plumbing identity, ignored by renderers.
- LayeredLayoutAlgorithm tags each emitted LayoutPort with its originating
  emission.SourcePort/TargetPort.
- HierarchicalLayoutAlgorithm.BuildSizedView now also returns the view-clone
  port map, and the hierarchical path translates each placed port's SourcePort
  from the sized-view clone back to the original scope's LayoutGraphPort before
  boundary-port resolution runs (the leaf pass runs over a cloned view graph
  whose ports are distinct objects from the original scope's ports).
- BoundaryPortResolver.FindLeafAnchors and ResolveInteriorTargets's nested-port
  branch now match by ReferenceEquals(..., SourcePort) instead of ExternalLabel
  string equality; ResolveOne tags its emitted anchor with SourcePort so
  delegation chains resolve by identity too.

Adds regression tests asserting connector *provenance* (not just anchor count/
label pairing) for shared-null and shared-identical ExternalLabel cases, both
at the BoundaryPortResolver unit level and the full HierarchicalLayoutAlgorithm
pipeline level. Updates reqstream/verification companion docs accordingly.

Fixes the BLOCKING finding in .agent-logs/quality-stage0-1-hierarchical-ports-4e2a91.md,
per the retry plan in .agent-logs/planning-retry1-boundary-port-identity-fix-9b4f2d.md.
BuildLinePath emitted a shortened-line+arc pair unconditionally for every
interior waypoint, even when the incoming and outgoing directions were
parallel and same-sense (not a real corner). Hoist the inline 0.001
tolerance literal into a named DirectionTolerance constant and add a
collinearity check (normalized cross/dot product) before the existing
degenerate-length check, so a collinear waypoint continues straight
through with a plain L command instead of a spurious rounded bump.

Add a regression test asserting no arc command is emitted for a
collinear interior waypoint, and document the collinear-waypoint skip
in the SvgRenderer unit design/verification docs and reqstream test
list.
An Internal-face hierarchy-crossing dummy (the shared fan-out point a
boundary port's delegation edges route through) was centered by
BrandesKopfPlacer purely among its own level's interior fan-out
targets, ignoring the parent scope's already-resolved anchor position
for the same boundary port. This produced an unnecessary extra
back-and-forth detour for whichever delegation target sat nearer the
anchor.

Add an optional PinnedCrossAxis field to AugNode (defaulting to null,
a byte-identical no-op for every non-boundary-port node and the flat
pipeline). MergeRegionGraphAssembler.BuildExternalCrossingIndex and
PinIncomingCrossings compute the parent level's already-placed
cross-axis offset (relative to the port's own container node, since
each level has its own local coordinate origin) and seed it onto the
matching Internal-face crossing dummy once the parent level has been
placed - guaranteed by LayeredLayoutPipeline.RunRecursive's existing
parent-before-child level iteration order (a consequence of
BuildAllLevelGraphs' pre-order population and Dictionary's
insertion-order preservation). BrandesKopfPlacer.AssignCoordinatesAug
then overrides just that node's cross-axis coordinate with the pinned
value after ordinary alignment/compaction.

Add a regression test reproducing the dispatch to Driver/Logger
scenario, asserting both delegation connectors take a minimal-bend
path with no direction reversal.
LayeredCorridorRouter.CreateDependency's crossing-count tie-break
forced two segments with an equal, non-zero crossing count into a
strict left/right routing-slot order purely by insertion order, even
when both segments converge on essentially the same target Y (a
symmetric fan-in, e.g. two external approaches converging on one
shared boundary anchor). This produced asymmetric clearance around the
shared point driven by insertion order rather than any genuine
geometric preference.

Skip creating a dependency for a pair whose target Y values coincide
within StraightTolerance, so the topological slot numbering does not
arbitrarily separate them.

Add a regression test with two mirror-symmetric approach edges
converging on one shared target, asserting identical first-bend
offsets; verified the test fails (asymmetric offsets) against the
prior tie-break and passes with the fix.
Document the Internal-face crossing dummy's PinnedCrossAxis anchoring
and LayeredCorridorRouter's shared-target-convergence tie-break
exception in the layered-pipeline unit design doc, and add the two new
regression tests to the BoundaryPortResolution requirement's reqstream
test list and verification doc.
Regenerated via gallery.ps1 after the collinear-waypoint,
pinned-anchor, and symmetric-fan-in fixes. The three boundary-port
showcase SVGs (deep-chain, horizontal, vertical) show the corrected
geometry; every other gallery SVG is byte-identical in content (only
line-ending metadata changed).
LayeredCorridorRouter.CreateDependency's crossing-count tie-break
guard already skipped the arbitrary left/right tie-break when two
segments shared a coincident TargetY (symmetric fan-in, fixed in
3069b22), but had no mirror check for a shared SourceY (symmetric
fan-out). dispatch->Driver and dispatch->Logger in the horizontal
boundary-ports showcase share the same SourceY (the dispatch anchor)
with differing TargetY, so the tie-break still fired and forced an
arbitrary insertion-order fork split (16px asymmetry).

Extend the guard's condition to also require the source Y values to
differ by more than StraightTolerance, so a pair sharing a coincident
anchor at either end - target (fan-in) or source (fan-out) - is left
without a forced separation.

Add OrthogonalRouter_Apply_MirrorSymmetricDivergingEdges_ProduceIdenticalFirstBendOffsets,
mirroring the existing fan-in regression test, asserting identical
first-bend offsets for a shared-source fan-out. Regenerate gallery
SVGs (only boundary-ports-showcase-horizontal.svg changed; vertical
and deep-chain galleries are unaffected). Document the mirror fix in
the layered-pipeline design/verification docs and reqstream test
list.
…cases

Add PNG raster companions for 8 gallery diagrams (via GalleryWriter.Png),
following the existing layered-pipeline/hierarchical-nested PNG pattern:

- boundary-ports-showcase-horizontal/vertical/deep-chain (required)
- ports-showcase-horizontal/vertical
- parallel-edges-merged/preserved/preserved-vertical

Changes:
- GalleryCatalog.cs: add 8 PNG filename consts and GalleryImage entries in
  the 'Raster output' section
- GalleryShowcaseTests.cs: add 8 dedicated Gallery_{Diagram}_RendersPng
  facts, mirroring the existing SVG facts' factories and Themes.Dark
- docs/gallery/README.md and 8 new docs/gallery/*.png: regenerated via
  gallery.ps1

Purely additive: no rendering logic changes.
Remove the 8 GalleryImage catalog entries for
boundary-ports-showcase-horizontal/-vertical/-deep-chain,
ports-showcase-horizontal/-vertical, and
parallel-edges-merged/-preserved/-preserved-vertical PNGs from the
Raster output section. Their [Fact] tests still write the .png files
to disk as quiet siblings of the .svg diagrams, but they no longer
appear in docs/gallery/README.md. The 8 filename consts are kept
since the tests still reference them. ThemeLight/Dark/Print and
LayeredPipeline/HierarchicalNested PNG entries are untouched.
Copilot AI review requested due to automatic review settings July 9, 2026 21:30
@Malcolmnixon
Malcolmnixon merged commit a692e8c into main Jul 9, 2026
6 checks passed
@Malcolmnixon
Malcolmnixon deleted the feature/hierarchical-boundary-ports branch July 9, 2026 21:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Rendering’s hierarchical layout and rendering model to support boundary (delegation) ports and recursive, ELK-style compound-graph layout in a single combined pass, with updated rendering behavior (dual port labels) and expanded verification/docs/gallery coverage.

Changes:

  • Introduces boundary-port detection + recursive merge-region assembly, recursive layered pipeline execution, and decomposition back into per-scope LayoutTree geometry (including cascading container sizing).
  • Updates the layout/rendering model for ports to support external + internal labels (boundary ports) and propagates this through SVG + Skia renderers, with additional routing tie-break logic for symmetric fan-in/fan-out.
  • Adds substantial new unit/integration tests plus documentation, requirements trace updates, and new gallery showcase diagrams for boundary ports.

Reviewed changes

Copilot reviewed 51 out of 62 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/DemaConsulting.Rendering.Tests/LayoutTests.cs Updates LayoutPort construction test to assert ExternalLabel/InternalLabel.
test/DemaConsulting.Rendering.Svg.Tests/SvgRendererPortedTests.cs Adds tests for collinear waypoint rounding + dual-label boundary port rendering rules.
test/DemaConsulting.Rendering.Skia.Tests/SkiaPortAndContentInsetTests.cs Adds raster test asserting dual-label boundary ports draw on both sides.
test/DemaConsulting.Rendering.Layout.Tests/LayeredLayoutAlgorithmTests.cs Updates port label assertions to use ExternalLabel.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/OrthogonalRouterTests.cs Adds symmetry regression tests for corridor-router slot tie-breaking.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/MergeRegionGraphAssemblerTests.cs New tests for merge-region structural assembly and recursion depth.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/MergeRegionDecomposerTests.cs New end-to-end orthogonality + minimal-bend regression tests via hierarchical layout.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/LongEdgeSplitterTests.cs Adds test ensuring hierarchy-crossing tags survive long-edge splitting.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/LayeredLayoutPipelineTests.cs Updates pipeline test to assert recursive pipeline can be built and run.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/HierarchyMergeRegionBuilderTests.cs New tests for boundary-port structural detection and recursive collection.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/CrossingMinimizerRecursionTests.cs New tests for recursive crossing minimization with up/down propagation effects.
test/DemaConsulting.Rendering.Layout.Tests/Engine/Layered/BoundaryPortResolverTests.cs New tests for direction→face mapping for boundary ports.
test/DemaConsulting.Rendering.Gallery/GalleryShowcaseTests.cs Adds gallery render tests for boundary-port showcase diagrams (SVG/PNG).
test/DemaConsulting.Rendering.Gallery/GalleryDiagrams.cs Adds boundary-port showcase graph builders (horizontal/vertical/deep chain).
test/DemaConsulting.Rendering.Gallery/GalleryCatalog.cs Adds gallery catalog entries/filenames for new boundary-port artifacts.
src/DemaConsulting.Rendering/LayoutTree/LayoutPort.cs Extends LayoutPort to ExternalLabel/InternalLabel + SourcePort plumbing.
src/DemaConsulting.Rendering/Graph/LayoutGraphPort.cs Updates port docs to reflect boundary-port semantics (external/internal labels + fan-out).
src/DemaConsulting.Rendering.Svg/SvgRenderer.cs Skips arc rounding at collinear waypoints; renders dual port labels (inward/outward).
src/DemaConsulting.Rendering.Skia/SkiaRasterRenderer.cs Mirrors SVG dual-label rendering behavior in Skia raster output.
src/DemaConsulting.Rendering.Layout/LayeredLayoutAlgorithm.cs Emits LayoutPort with SourcePort identity preserved for later reconciliation.
src/DemaConsulting.Rendering.Layout/HierarchicalLayoutAlgorithm.cs Gates boundary-port combined pass; runs recursive pipeline with cascading sizing; maps view-port identity back to original ports.
src/DemaConsulting.Rendering.Layout/Engine/Layered/MergeRegionDecomposer.cs New: projects recursive placement into LayoutBox/LayoutLine/LayoutPort geometry with shared anchors and orthogonal joins.
src/DemaConsulting.Rendering.Layout/Engine/Layered/LongEdgeSplitter.cs Preserves pre-seeded hierarchy-crossing tags during augmented-node rebuild.
src/DemaConsulting.Rendering.Layout/Engine/Layered/LayeredLayoutPipeline.cs Adds RunRecursive, crossing tagging, and AddRecursiveStages support; removes recursive build rejection.
src/DemaConsulting.Rendering.Layout/Engine/Layered/LayeredGraph.cs Adds hierarchy-crossing descriptors (HierarchyCrossingFace, HierarchyCrossing) and pinning support (PinnedCrossAxis).
src/DemaConsulting.Rendering.Layout/Engine/Layered/LayeredCorridorRouter.cs Adjusts tie-break dependency creation to avoid asymmetric slot separation at shared anchors.
src/DemaConsulting.Rendering.Layout/Engine/Layered/LayerAssigner.cs Adds recursive layer assignment across merge-region levels.
src/DemaConsulting.Rendering.Layout/Engine/Layered/HierarchyMergeRegionBuilder.cs New: structural detection/collection of boundary (delegation) ports per scope and recursively.
src/DemaConsulting.Rendering.Layout/Engine/Layered/HierarchyHandling.cs Updates enum docs to reflect implemented recursive combined-pass behavior.
src/DemaConsulting.Rendering.Layout/Engine/Layered/CrossingMinimizer.cs Adds recursive crossing minimization with up-sweep/down-sweep propagation and pinned-incoming mode.
src/DemaConsulting.Rendering.Layout/Engine/Layered/BrandesKopfPlacer.cs Honors PinnedCrossAxis for hierarchy-crossing dummies during placement.
src/DemaConsulting.Rendering.Layout/Engine/Layered/BoundaryPortResolver.cs New: direction→boundary-face mapping used by the combined pass/decomposer.
README.md Adds high-level Features section including boundary ports and recursive hierarchy support.
docs/verification/rendering/layout-tree.md Updates verification text for dual-label LayoutPort.
docs/verification/rendering-svg/svg-renderer.md Updates verification coverage to include collinear-waypoint rounding behavior.
docs/verification/rendering-layout/hierarchical-layout-algorithm.md Expands verification to include boundary-port combined pass and sizing cascade behaviors.
docs/verification/rendering-layout/engine/layered-pipeline.md Updates verification scope to cover recursive pipeline and boundary-port decomposition behavior.
docs/user_guide/introduction.md Adds user-facing boundary (delegation) ports section with example.
docs/reqstream/rendering/layout-tree.yaml Updates requirement to include external/internal port labels.
docs/reqstream/rendering-svg/svg-renderer.yaml Adds requirement coverage for collinear-waypoint no-arc behavior.
docs/reqstream/rendering-layout/hierarchical-layout-algorithm.yaml Adds requirements + tests for boundary-port delegation and revised error behavior.
docs/reqstream/rendering-layout/engine/layered-pipeline.yaml Adds requirements + tests for recursive combined pass and boundary-port detection/resolution.
docs/reqstream/rendering-layout.yaml Registers new layered-pipeline and hierarchical-layout requirement IDs.
docs/gallery/README.md Adds new “Boundary and delegation ports” gallery section.
docs/gallery/boundary-ports-showcase-vertical.svg New generated gallery artifact for vertical boundary-port showcase.
docs/gallery/boundary-ports-showcase-horizontal.svg New generated gallery artifact for horizontal boundary-port showcase.
docs/gallery/boundary-ports-showcase-deep-chain.svg New generated gallery artifact for deep-chain boundary-port showcase.
docs/design/rendering/layout-tree.md Updates design description of LayoutPort with dual-label semantics.
docs/design/rendering-svg/svg-renderer.md Updates design notes for collinear-waypoint rounding behavior.
docs/design/rendering-layout/hierarchical-layout-algorithm.md Updates design to describe combined pass, boundary ports, and cascading sizing.
docs/design/rendering-layout/engine/layered-pipeline.md Updates design to describe implemented recursive pipeline + boundary-port mechanics.
.cspell.yaml Adds new allowed terms related to routing/geometry changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +995 to +999
/// Emits one port <c>&lt;text&gt;</c> label offset from the port centre toward the interior of the
/// box on <paramref name="offsetSide"/> (which equals the port's own side for an inward label and the
/// opposite side for an outward one), using the same offset formula for both so an inward and an
/// outward label on one boundary port sit symmetrically about the port centre.
/// </summary>
Comment on lines +1285 to +1289
/// Draws one port label offset from the port centre using the interior-side formula for
/// <paramref name="offsetSide"/> (the port's own side for an inward label, the opposite side for an
/// outward one), so an inward and an outward label on one boundary port sit symmetrically about the
/// port centre.
/// </summary>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants