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
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Developer Agent Report

**Result**: SUCCEEDED
**Report**: `.agent-logs/developer-connector-router-quality-retry-20260704-2205.md`

- **Files Modified**:
- `src/DemaConsulting.Rendering.Layout/ConnectorRouter.cs`
- `test/DemaConsulting.Rendering.Layout.Tests/ConnectorRouterTests.cs`
- `docs/reqstream/rendering-layout/connector-router.yaml`
- `docs/design/rendering-layout/connector-router.md`
- `docs/verification/rendering-layout/connector-router.md`
- `docs/user_guide/introduction.md`
- `.agent-logs/developer-connector-router-quality-retry-20260704-2205.md`
- **Languages Detected**: C#, Markdown, YAML
- **Standards Applied**:
- `docs/design/introduction.md`
- `.github/standards/coding-principles.md`
- `.github/standards/csharp-language.md`
- `.github/standards/testing-principles.md`
- `.github/standards/csharp-testing.md`
- `.github/standards/requirements-principles.md`
- `.github/standards/reqstream-usage.md`
- `.github/standards/software-items.md`
- `.github/standards/design-documentation.md`
- `.github/standards/verification-documentation.md`
- `.github/standards/technical-documentation.md`

## Tooling Executed

- **Language Tools**:
- `dotnet test test/DemaConsulting.Rendering.Layout.Tests/`
`DemaConsulting.Rendering.Layout.Tests.csproj --no-restore --nologo`
- `pwsh ./lint.ps1`
- `pwsh ./build.ps1`
- **Compliance Tools**:
- `lint.ps1` checks: `markdownlint-cli2`, `cspell`, `yamllint`, `dotnet format`,
`reqstream`, `reviewmark`, `versionmark`
- **Validation Results**:
- Targeted layout tests passed for `net8.0`, `net9.0`, and `net10.0`
- `pwsh ./lint.ps1` passed
- `pwsh ./build.ps1` passed with zero warnings/errors and successful full test execution

## Compliance Status

- **Quality Checks**: Required targeted test, lint, and build validations all passed;
`fix.ps1` was not needed because lint passed without formatting changes.
- **Issues Resolved**: `ConnectorRouter` now resolves folder tab width and height hints
independently so partial folder hints stay shape-aware, regression coverage documents
width-only and height-only cases, and the required reqstream/design/verification/user-guide
artifacts were updated. Unrelated gallery worktree changes remained untouched and were
excluded from the commit.
1 change: 1 addition & 0 deletions .reviewmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ reviews:
- "docs/design/rendering-layout/engine/engine.md"
- "docs/reqstream/rendering-layout.yaml"
- "docs/reqstream/rendering-layout/engine/engine.yaml"
- "docs/design/rendering-layout/connector-router.md"

# Rendering.Layout system-level units
- id: Rendering-Layout-LayeredLayoutAlgorithm
Expand Down
54 changes: 48 additions & 6 deletions docs/design/rendering-layout/connector-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,38 @@ It mirrors ELK's `elk.edgeRouting` and today carries the single value `Orthogona
exposed on the open property system as `CoreOptions.EdgeRouting` (id `rendering.edgerouting`, default
`Orthogonal`), so routing can be selected per scope alongside `CoreOptions.Algorithm`.

Internally, `ConnectorRouter` also resolves one shape-geometry object per endpoint box. That internal
abstraction exposes two face-level concerns per `PortSide`: the connectable extents (one or more
usable sub-ranges along the face, possibly empty) and the surface projection (the inward offset from
the bounding-box face to the real outline at a chosen along-face coordinate). The shipped
implementations are:

- **Rectangle** — one full-length extent per face; zero projection on every face.
- **RoundedRectangle** — one extent per face inset by the resolved corner radius at both ends; zero
projection because the flat portion of the face still lies on the bounding box.
- **Note** — one full-length extent per face for now; zero projection.
- **Folder** — full-length left, right, and bottom extents; top extent only to the right of the
raised tab, and a positive top-face projection equal to the resolved tab height so the anchor
touches the recessed body top. `FolderTabWidth` and `FolderTabHeight` are resolved
independently, so when only one hint is supplied the router computes the missing companion
dimension from its generic fallback before extents and projection are evaluated.

### ConnectorRouter Methods

`Route(boxes, connection, options)` rejects null arguments (including a null `From` or `To`) with
`ArgumentNullException`, then:

1. **Anchor selection.** Computes each box centre and, for each endpoint, chooses the midpoint of the
box side whose outward normal best points at the opposing box centre (right/left when the
horizontal separation dominates, otherwise bottom/top). The chosen `PortSide` is retained so the
route exits and enters perpendicular to the face.
1. **Anchor selection.** Computes the naturally-facing source and target faces from box separation as
before (right/left when the horizontal separation dominates, otherwise bottom/top). For each
endpoint, if that natural face reports a non-empty connectable extent it is used; otherwise the
router falls back, in order, to the adjacent face that still points most toward the other box on
the minor axis, then the other adjacent face, and finally the opposite face as a last resort.
The along-face coordinate is chosen from the overlap-centre rule used previously, then clamped
inward by `ConnectorRouteOptions.Clearance` whenever the face is long enough to keep that margin
from both ends. Faces too short for that inset fall back to their own center instead of violating
the margin. The result is then clamped into the chosen face's usable connectable extents (again
applying the same clearance inset when an extent is long enough) and projected inward to the real
outline. The chosen `PortSide` is retained so the route exits and enters perpendicular to the face.
2. **Obstacle set.** Builds a `Rect` per box, excluding the connection's two endpoint boxes matched
by reference identity, so the connector is free to leave and enter the boxes it joins.
3. **Dispatch.** Routes through the router realizing `options.EdgeRouting`. Today `Orthogonal` maps to
Expand All @@ -48,8 +71,17 @@ exposed on the open property system as `CoreOptions.EdgeRouting` (id `rendering.
4. **Assembly.** Wraps the returned waypoints in a `LayoutLine` carrying the connection's `TargetEnd`,
`LineStyle`, and `Label`, with `SourceEnd` left `None`.

The batch overload applies the single-connection routine to each connection and returns one line per
connection in input order.
The batch overload first computes those naive per-connection face coordinates, then groups any shared
box face claims and redistributes them across the **union of that face's connectable extents** rather
than across the full bounding-box span. It still orders the claims by counterpart-box centre so the
visual left-to-right or top-to-bottom order of the connectors tracks the order of their counterparts.
Finally it routes the connectors sequentially and turns only each prior route's **interior** segments
into soft obstacles; the short endpoint-adjacent approach legs are intentionally omitted so several
connectors may still share a legitimate final corridor into the same box face without being lured into
redundant leave-and-return detours. The underlying router now discourages an interior overlap in
proportion to how far it extends, rather than treating every overlap as an equally cheap flat cost, so
parallel fan-out or fan-in connectors separate into visually distinct corridors instead of merging
along a shared trunk for an extended span.

### ConnectorRouter Error Handling

Expand Down Expand Up @@ -81,6 +113,14 @@ them:
- **External application code** — any caller that supplies its own placed `LayoutBox` list (for
example from a containment or free-form placement produced outside the layered pipeline) and needs
routed `LayoutLine` connectors to drop into a `LayoutTree`.
- **LayeredPipeline** — the `PortDistributor` and `LongEdgeJoiner` stages internally consume
`ConnectorRouter`'s (internal, widened-accessibility) shape-geometry resolution
(`ResolveShapeGeometry` and its supporting extent-math helpers) for same-scope (leaf) edges routed
through `LayeredLayoutAlgorithm`'s own pipeline, so a shaped node gets the same connectable-extent
restriction and inward surface projection whether it is routed by `ConnectorRouter` (cross-container
edges) or by the layered pipeline (same-scope edges). See _Layered Pipeline Unit Design_'s
"Layered Pipeline Dependencies" section for the reverse-direction documentation of this cross-unit
dependency.

### ConnectorRouter Interactions

Expand All @@ -95,6 +135,8 @@ any set of placed boxes.
| Requirement ID | Satisfied by |
| --- | --- |
| Rendering-Layout-ConnectorRouter-AnchorsFaceEachOther | ConnectorRouter behavior described above |
| Rendering-Layout-ConnectorRouter-ShapeAwareAnchors | ConnectorRouter behavior described above |
| Rendering-Layout-ConnectorRouter-SharedFaceDistribution | ConnectorRouter behavior described above |
| Rendering-Layout-ConnectorRouter-AvoidsObstacles | ConnectorRouter behavior described above |
| Rendering-Layout-ConnectorRouter-ExcludesEndpoints | ConnectorRouter behavior described above |
| Rendering-Layout-ConnectorRouter-CarriesStyling | ConnectorRouter behavior described above |
Expand Down
30 changes: 16 additions & 14 deletions docs/design/rendering-layout/default-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ registry is safe to read (resolve) concurrently.

### DefaultLayout Methods

`LayoutEngine.Layout(graph, options)` resolves against the shared default registry;
`LayoutEngine.Layout(graph, options, registry)` resolves against a caller-supplied registry. Both reject
`LayoutEngine.Layout(graph)` resolves against the shared default registry;
`LayoutEngine.Layout(graph, registry)` resolves against a caller-supplied registry. Both reject
null arguments with `ArgumentNullException`, then:

1. **Resolve the algorithm identifier.** The identifier is read from an explicit `CoreOptions.Algorithm`
on the graph, else from an explicit `CoreOptions.Algorithm` on the options, else `DefaultAlgorithmId`
(`"hierarchical"`). Resolution consults *explicit* settings only (via `TryGet`), so an unset graph and
options fall through to the hierarchical default rather than the `CoreOptions.Algorithm` property
default of `"layered"`. The graph takes precedence over the options because, in the ELK-style model,
layout options are naturally attached to the graph being laid out.
set directly on the graph, else `DefaultAlgorithmId` (`"hierarchical"`). Resolution consults an
*explicit* graph setting only (via `TryGet`), so an unset graph falls through to the hierarchical
default rather than the `CoreOptions.Algorithm` property default of `"layered"`. The graph is the
single place to configure a layout — since `LayoutGraph` is itself an `IPropertyHolder` — so there is
no second, free-standing options object at this entry point that could disagree with it.
2. **Resolve and apply.** The identifier is resolved from the registry and the resolved algorithm's
`Apply(graph, options)` produces the placed `LayoutTree`.
`Apply(graph, options)` produces the placed `LayoutTree`, where `options` is an empty `LayoutOptions`
used only to seed the algorithm's internal option-cascading contract (see the respective algorithm's
Unit Design document); the graph itself already carries every explicit setting.

Defaulting to the hierarchical engine is what lets the single facade serve both flat and nested graphs.
It is safe because of the hierarchical engine's flat-graph equivalence guarantee: for a graph with no
Expand All @@ -51,12 +53,12 @@ would, while a nested graph is composed correctly — with no decision required
- The facade shall default to the hierarchical engine, not the layered algorithm, so one entry point
handles both flat and nested graphs; the flat-graph equivalence guarantee makes this behavior-
preserving.
- The facade shall consult only explicit algorithm declarations when resolving, so an unset graph and
options reach the hierarchical default rather than the layered property default.
- The facade shall consult only an explicit algorithm declaration on the graph when resolving, so an
unset graph reaches the hierarchical default rather than the layered property default.

### DefaultLayout Error Handling

Null `graph`, `options`, or (three-argument overload) `registry` throw `ArgumentNullException`. A
Null `graph` or (two-argument overload) `registry` throw `ArgumentNullException`. A
declared algorithm identifier absent from the resolving registry surfaces the registry's
`KeyNotFoundException`.

Expand All @@ -67,7 +69,7 @@ declared algorithm identifier absent from the resolving registry surfaces the re
- **Rendering.Abstractions** (`LayoutAlgorithmRegistry`, `ILayoutAlgorithm`) — the registry type
populated by `CreateDefaultRegistry` and the algorithm contract resolved and invoked by
`LayoutEngine.Layout`.
- **Rendering model** (`DemaConsulting.Rendering`) — the `LayoutGraph`, `LayoutOptions`, and
- **Rendering model** (`DemaConsulting.Rendering`) — the `LayoutGraph` and
`LayoutTree` types on the public `Layout` signature, plus `CoreOptions.Algorithm` used for
algorithm-identifier resolution.
- **Layout units** (`LayeredLayoutAlgorithm`, `ContainmentLayoutAlgorithm`,
Expand All @@ -80,8 +82,8 @@ No OTS runtime component or shared package is consumed.

`LayoutAlgorithms` and `LayoutEngine` are consumed by:

- **External application code** — the primary caller. Applications invoke `LayoutEngine.Layout(graph,
options)` (or the three-argument overload with a custom registry) as the batteries-included happy
- **External application code** — the primary caller. Applications invoke `LayoutEngine.Layout(graph)`
(or the two-argument overload with a custom registry) as the batteries-included happy
path for going from `LayoutGraph` to placed `LayoutTree` with a single call.
- **Renderer host code** (for example downstream of `SvgRenderer` / `PngRenderer`) — callers that
pair `LayoutEngine.Layout(...)` with an `IRenderer` to go from graph to rendered output in two
Expand Down
35 changes: 31 additions & 4 deletions docs/design/rendering-layout/engine/layered-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ per-sub-edge port positions, the per-sub-edge bend points, and finally the assem
waypoints. The `BackEdgeEntryApproach` parameter (default `ConnectorClearance`) lets a
decoration-aware caller lengthen a reversed edge's final approach without disturbing default
geometry. The `SwapNodeAxes` seam swaps each node's width and height for the down/up directions so
the direction-agnostic stages space layers by the correct extent.
the direction-agnostic stages space layers by the correct extent; it preserves every other
`LayerNode` field (`Shape`, `RoundedCornerRadius`, `FolderTabWidth`, `FolderTabHeight`, `Label`,
`RealWidth`, `RealHeight`) unchanged, since only the abstract along/cross axes need to reorient.

`LayerNode` carries shape metadata alongside its abstract `Width`/`Height`: `Shape` (defaulting to
`BoxShape.Rectangle`, which keeps every 2-arg call site's byte-identical full-face behavior),
`RoundedCornerRadius`, `FolderTabWidth`, `FolderTabHeight`, and `Label` mirror the corresponding
`LayoutGraphNode`/`LayoutBox` properties, and `RealWidth`/`RealHeight` carry the node's true,
never-swapped bounding-box dimensions (needed because `Width`/`Height` may have been swapped by
`SwapNodeAxes`, but shape geometry such as a note's fold size combines both real dimensions
independently of the abstract axes).

Each stage implements `ILayoutStage` (`void Apply(LayeredGraph graph)`) and mutates the graph in
place. Stages are stateless and may be shared across pipelines. `LayeredLayoutMetrics` holds the
Expand Down Expand Up @@ -61,13 +71,22 @@ The default stage sequence added by `AddDefaultStages` runs in this order:
source-side and target-side port that lies within the corresponding node face. The clearance inset
is capped at half the face extent so a box too small to hold the full clearance on both edges
degrades gracefully (ports collapse toward the centre) rather than inverting the clamp range; a
box at least twice the clearance tall is unaffected, keeping its geometry byte-identical.
box at least twice the clearance tall is unaffected, keeping its geometry byte-identical. For a
non-`BoxShape.Rectangle` real node, the port band is further restricted to the shape's usable
connectable extents on the resolved real face (proportionally distributed across multiple
disjoint extents when the shape excludes a middle portion of the face), reusing `ConnectorRouter`'s
shape-geometry resolution via `ShapeAnchorSupport`; a plain-`Rectangle` node keeps the original
full-span formula untouched.
7. **LayeredCorridorRouter.** Assigns routing slots per corridor and emits orthogonal bend points, adding
no bend points for a straight sub-edge. It reads `BackEdgeEntryApproach` to reserve a minimum
final approach for a reversed edge; at the default this clamp is a no-op, keeping forward geometry
byte-identical.
8. **LongEdgeJoiner.** Concatenates the bend points of a split edge's sub-edges into one polyline per
original edge.
original edge. For a non-`BoxShape.Rectangle` real endpoint, the assembled perpendicular endpoint
coordinate is additionally projected inward by the shape's surface-projection offset at that
endpoint's local face coordinate (for example a folder's tab height), so the connector touches the
shape's real outline rather than the plain bounding-box edge; a plain-`Rectangle` endpoint skips
geometry resolution entirely and keeps the original formula byte-identical.
9. **AxisTransform.** Maps the abstract left-to-right along/cross coordinates onto screen coordinates
for the requested direction. The Right direction is the identity; Down, Left, and Up are rotations
or flips. It also normalizes the input node axes at the start of `Run`.
Expand All @@ -85,7 +104,14 @@ or several.
All pipeline types are internal and consume only the geometric value types of the Layout system
(`Point2D`, `Rect`) plus the internal `LayerNode`, `LayerEdge`, `AugNode`, and `AugEdge` records.
No stage depends on the semantic `LayoutGraph` model, any OTS runtime component, or any Shared
Package.
Package. The one exception is the `PortDistributor` and `LongEdgeJoiner` stages' dependency, via the
small internal `ShapeAnchorSupport` helper, on `ConnectorRouter`'s internal shape-geometry types
(`IBoxShapeGeometry`, `ResolveShapeGeometry`, `BuildUsableExtents`, `TotalExtentLength`,
`CoordinateAtDistance`) for non-`BoxShape.Rectangle` nodes — a documented cross-unit dependency
(`LayeredPipeline` unit → `ConnectorRouter` unit) that lets a shaped node's ports and endpoints reuse
`ConnectorRouter`'s already-tested extent-restriction and surface-projection rules instead of
duplicating them. See _ConnectorRouter Unit Design_'s "Callers" section for the reverse-direction
documentation of this dependency.

#### Layered Pipeline Callers

Expand Down Expand Up @@ -121,3 +147,4 @@ public layout result contract.
| Rendering-Layout-LayeredPipeline-PackedComponentsBackEdgeApproach | Layered pipeline behavior described above |
| Rendering-Layout-LayeredPipeline-SharedState | Layered pipeline behavior described above |
| Rendering-Layout-LayeredPipeline-InputValidation | Layered pipeline behavior described above |
| Rendering-Layout-LayeredPipeline-ShapeAwareAnchors | Layered pipeline behavior described above |
Loading
Loading