Enhance boundary port handling and layout pipeline features#14
Merged
Conversation
added 23 commits
July 8, 2026 19:53
…caffolding docs (Stage 0)
…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.
…, verification, README, and user guide
…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.
…mposition (fixes fan-in diagonal defect)
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.
There was a problem hiding this comment.
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
LayoutTreegeometry (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><text></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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
Routing and Tie-Break Improvements
Error Handling and API Contracts
NotSupportedException; boundary ports on containers are now fully supported. [1] [2]Other Documentation and Spelling
README.mdhighlighting the pluggable algorithms, orthogonal routing, named ports, boundary ports, and multiple output formats.