Add ElkSharp rendering architecture docs, ADRs, tutorial, AGENTS rules
Five documentation deliverables for the ElkSharp rendering improvements: 1. docs/workflow/engine/16-elksharp-rendering-architecture.md (453 lines) Full pipeline: Sugiyama stages, edge routing strategies, hybrid deterministic mode, gateway geometry, 18-category scoring system, corridor routing, Y-gutter expansion, diagnostics. 2. docs/workflow/engine/17-elksharp-architectural-decisions.md (259 lines) Six ADRs: short-stub normalization, gateway vertex entries, Y-gutter expansion, corridor rerouting, FinalScore adjustment, alongside detection. 3. docs/workflow/tutorials/10-rendering/README.md (234 lines) Practical tutorial: setup, layout options, SVG/PNG rendering, diagnostics capture, violation reports, full end-to-end example. 4. src/__Libraries/StellaOps.ElkSharp/AGENTS.md — 7 new local rules for Y-gutter, corridor reroute, gateway vertices, FinalScore adjustments, short-stub normalization, alongside detection, target-join spread. 5. docs/workflow/ENGINE.md — replaced monolithic ElkSharp paragraph with structured pipeline overview, effort-level table, and links to the new architecture docs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
453
docs/workflow/engine/16-elksharp-rendering-architecture.md
Normal file
453
docs/workflow/engine/16-elksharp-rendering-architecture.md
Normal file
@@ -0,0 +1,453 @@
|
||||
# ElkSharp Rendering Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
ElkSharp is a deterministic, in-process Sugiyama-based graph layout engine for workflow
|
||||
visualization. It replaces external layout dependencies (ElkJs/Node.js) with a pure C#
|
||||
implementation that produces identical output for identical input, regardless of host
|
||||
platform, thread scheduling, or execution environment.
|
||||
|
||||
The engine handles the full pipeline from abstract workflow graphs to positioned nodes
|
||||
and routed edges, suitable for SVG/PNG/JSON rendering. It supports left-to-right and
|
||||
top-to-bottom layout directions, three effort levels (Draft, Balanced, Best), compound
|
||||
node hierarchies, and gateway-specific polygon geometry (diamond decisions, hexagon
|
||||
forks/joins).
|
||||
|
||||
---
|
||||
|
||||
## Sugiyama Pipeline
|
||||
|
||||
The core layout algorithm follows the Sugiyama framework for layered graph drawing,
|
||||
extended with workflow-specific constraints.
|
||||
|
||||
### 1. Layer Assignment
|
||||
|
||||
Depth-first traversal assigns a layer index to each node. The layer index determines
|
||||
the horizontal position (in left-to-right mode) or vertical position (in top-to-bottom
|
||||
mode) of each node.
|
||||
|
||||
- Start nodes are assigned layer 0.
|
||||
- Each successor is assigned `max(predecessor layers) + 1`.
|
||||
- Backward edges (cycles, repeat connectors) are identified and handled separately
|
||||
so they do not influence forward layer assignment.
|
||||
|
||||
### 2. Dummy Node Insertion
|
||||
|
||||
Edges that span more than one layer are split into chains of single-layer segments.
|
||||
Each intermediate layer receives a "dummy node" -- a zero-size virtual node that
|
||||
serves as a routing waypoint.
|
||||
|
||||
- Dummy nodes inherit the edge's channel classification (forward, backward, sink).
|
||||
- After routing, dummy chains are reconstructed back into the original multi-layer
|
||||
edge with bend points at each dummy position.
|
||||
|
||||
### 3. Node Ordering
|
||||
|
||||
Barycenter-based median ordering minimizes edge crossings within each layer.
|
||||
|
||||
- The algorithm sweeps forward and backward across layers, computing each node's
|
||||
barycenter (average position of connected nodes in adjacent layers).
|
||||
- Nodes are sorted by barycenter within their layer.
|
||||
- Iteration count depends on effort level:
|
||||
- Draft: 8 iterations
|
||||
- Balanced: 14 iterations
|
||||
- Best: 24 iterations
|
||||
- Tie-breaking is deterministic (stable sort by node ID) to ensure reproducibility.
|
||||
|
||||
### 4. Initial Placement
|
||||
|
||||
After ordering, nodes receive Y-coordinates (in left-to-right mode) based on the
|
||||
median of incoming connection Y-centers.
|
||||
|
||||
- Enforced linear spacing ensures minimum `NodeSpacing` between adjacent nodes.
|
||||
- Nodes with no incoming connections use the median of outgoing connection positions.
|
||||
- Grid alignment snaps positions to the nearest grid line for visual consistency.
|
||||
|
||||
### 5. Placement Refinement
|
||||
|
||||
Multiple refinement passes adjust positions to reduce visual clutter:
|
||||
|
||||
- **Preferred-center pull**: Nodes shift toward the center of their connected
|
||||
neighbors' Y-range, weighted by connection count.
|
||||
- **Snap-to-grid**: Positions align to a grid derived from `NodeSpacing`.
|
||||
- **Compact-toward-incoming**: Nodes pull toward their primary incoming edge
|
||||
direction to reduce edge length and crossings.
|
||||
|
||||
Refinement iteration count scales with effort level (3 / 6 / 10 for Draft / Balanced / Best).
|
||||
|
||||
### 6. Y-Gutter Expansion
|
||||
|
||||
After edge routing identifies under-node violations (edges running through or
|
||||
alongside nodes), the engine shifts entire Y-bands downward to create routing
|
||||
corridors.
|
||||
|
||||
- Runs after X-gutter expansion and before compact passes.
|
||||
- Scans routed edges for horizontal segments that violate under-node or alongside
|
||||
clearance rules.
|
||||
- Identifies blocking nodes and computes the required clearance gap.
|
||||
- Shifts ALL nodes below the violation Y downward by the clearance amount.
|
||||
This preserves relative ordering within each layer.
|
||||
- Re-routes edges with the expanded corridors.
|
||||
- Up to 2 iterations to handle cascading violations.
|
||||
|
||||
The key insight is that shifting individual nodes disrupts the Sugiyama median-based
|
||||
optimization, causing cascading layout degradation. Shifting entire Y-bands (like
|
||||
X-gutter expansion does for inter-layer gaps) preserves within-layer relationships.
|
||||
|
||||
### 7. X-Gutter Expansion
|
||||
|
||||
Inter-layer horizontal gaps are widened to provide edge corridor space.
|
||||
|
||||
- The base gap is `LayerSpacing` (default 60px).
|
||||
- When edge density between two layers exceeds a threshold, the gap is scaled
|
||||
up to 1.8x to accommodate additional routing channels.
|
||||
- Expansion is computed before edge routing so that the router has adequate space
|
||||
for orthogonal paths.
|
||||
|
||||
---
|
||||
|
||||
## Edge Routing
|
||||
|
||||
### Channel Assignment
|
||||
|
||||
Each edge is classified into one of three routing channels:
|
||||
|
||||
- **Forward**: Source layer < target layer (the common case).
|
||||
- **Backward**: Source layer > target layer (repeat/loop connectors).
|
||||
- **Sink**: Edges to terminal nodes (End events) that may use special corridors.
|
||||
|
||||
Channel classification determines routing priority, corridor eligibility, and
|
||||
post-processing rules.
|
||||
|
||||
### Base Routing
|
||||
|
||||
Orthogonal bend-point construction builds an initial route from source to target:
|
||||
|
||||
1. Exit the source node perpendicular to its boundary.
|
||||
2. Route horizontally through inter-layer gutters.
|
||||
3. Enter the target node perpendicular to its boundary.
|
||||
4. Insert 90-degree bends at each direction change.
|
||||
|
||||
The base router respects node obstacles, avoiding routes that cross through node
|
||||
rectangles or polygons.
|
||||
|
||||
### Dummy Edge Reconstruction
|
||||
|
||||
After routing, multi-layer dummy chains are merged back to original edges:
|
||||
|
||||
- Bend points from each dummy segment are concatenated.
|
||||
- Redundant collinear points are removed.
|
||||
- The result is a single edge with bend points at each layer transition.
|
||||
|
||||
### Anchor Snapping
|
||||
|
||||
Edge endpoints are projected onto actual node shape boundaries:
|
||||
|
||||
- **Rectangle**: Standard side intersection (left, right, top, bottom).
|
||||
- **Diamond** (decision gateways): Intersection with the diamond's four edges,
|
||||
producing diagonal approach stubs.
|
||||
- **Hexagon** (fork/join gateways): Intersection with the hexagon's six edges,
|
||||
with asymmetric shoulder geometry.
|
||||
|
||||
Anchor snapping runs after routing so that bend points near the boundary
|
||||
produce clean visual connections.
|
||||
|
||||
---
|
||||
|
||||
## Iterative Optimization (Hybrid Deterministic Path)
|
||||
|
||||
The `Best` effort level activates hybrid deterministic optimization, which repairs
|
||||
routing violations without disrupting the Sugiyama node placement.
|
||||
|
||||
### 1. Baseline Evaluation
|
||||
|
||||
The baseline route is scored using an 18-category violation taxonomy. Each edge
|
||||
receives a violation list with severity-weighted penalties. The total score is the
|
||||
sum of all edge scores.
|
||||
|
||||
### 2. Repair Planning
|
||||
|
||||
Penalized edges are identified from the violation severity map. The planner
|
||||
extracts the specific violations for each edge and determines which edges need
|
||||
repair and in what priority order.
|
||||
|
||||
High-severity violations (node crossings, under-node, shared lanes) take priority
|
||||
over medium-severity (backtracking, detours) and soft-severity (edge crossings,
|
||||
proximity, bends).
|
||||
|
||||
### 3. Conflict-Zone Batching
|
||||
|
||||
Independent repair candidates are grouped by shared source, shared target, or
|
||||
shared corridor zone. Edges in the same conflict zone are batched together so
|
||||
that their repairs are coordinated rather than competing.
|
||||
|
||||
Batching ensures that fixing one edge does not create a new violation for a
|
||||
nearby edge in the same zone.
|
||||
|
||||
### 4. Parallel A* Candidate Construction
|
||||
|
||||
Each repair batch constructs candidate reroutes using A* 8-direction pathfinding:
|
||||
|
||||
- Candidates are built on full-core parallel threads.
|
||||
- Each candidate is a complete reroute for the batch's edge set.
|
||||
- The A* grid derives intermediate spacing from approximately one-third of the
|
||||
average service-task node size.
|
||||
- Node-obstacle blocked step masks are precomputed per route so neighbor
|
||||
expansion does not rescan every node.
|
||||
- Merge back into the route is deterministic and single-threaded.
|
||||
|
||||
### 5. Winner Refinement
|
||||
|
||||
The best candidate from each batch undergoes a refinement pipeline:
|
||||
|
||||
1. **Under-node repair**: Shift lanes that pass through node bounding boxes.
|
||||
2. **Local-bundle spread**: Separate parallel edges that share the same lane.
|
||||
3. **Shared-lane elimination**: Push edges apart that overlap on the same axis.
|
||||
4. **Boundary-slot snap**: Align endpoints to the discrete slot lattice.
|
||||
5. **Detour collapse**: Remove unnecessary overshoots where a shorter path exists.
|
||||
6. **Post-slot restabilization**: Re-validate slot assignments after detour changes.
|
||||
7. **Corridor reroute**: Move long horizontal sweeps to top/bottom corridors.
|
||||
8. **Elevation adjustment**: Shift edges vertically to clear obstructions.
|
||||
9. **Target-join spread**: Push convergent approach lanes apart by
|
||||
`minClearance - currentGap + 8px` (half applied to each edge).
|
||||
|
||||
Winner promotion uses weighted score comparison (`Score.Value`) to ensure
|
||||
the refinement actually improved the layout.
|
||||
|
||||
---
|
||||
|
||||
## Gateway Geometry
|
||||
|
||||
Gateways use non-rectangular shapes that require specialized boundary logic.
|
||||
|
||||
### Decision Gateway (Diamond)
|
||||
|
||||
- 4 vertices: left tip, top, right tip, bottom.
|
||||
- Left and right tips are the horizontal extremes.
|
||||
- Top and bottom are the vertical extremes.
|
||||
- Source exits leave from face interiors (not tips).
|
||||
- Target entries may use left/right tips as convergence points.
|
||||
- `ForceDecisionSourceExitOffVertex` blocks source exits from tip vertices.
|
||||
|
||||
### Fork/Join Gateway (Hexagon)
|
||||
|
||||
- 6 vertices: left tip, upper-left shoulder, upper-right shoulder, right tip,
|
||||
lower-right shoulder, lower-left shoulder.
|
||||
- Shoulders create flat top and bottom faces suitable for multiple slot entries.
|
||||
- Asymmetric geometry: the shoulder offset from the tip varies by gateway size.
|
||||
|
||||
### Boundary Slot Capacity
|
||||
|
||||
| Shape | Face | Max Slots |
|
||||
|-------|------|-----------|
|
||||
| Gateway (diamond/hexagon) | Any face | 2 |
|
||||
| Rectangle | Left / Right | 3 |
|
||||
| Rectangle | Top / Bottom | 5 |
|
||||
|
||||
Slots are evenly distributed within the face's safe boundary inset. Scoring and
|
||||
final repair share the same realizable slot coordinates.
|
||||
|
||||
### Gateway Vertex Entry Rules
|
||||
|
||||
Left and right tip vertices are allowed for target entries but blocked for source
|
||||
exits. This is enforced by a 3-way coordination mechanism:
|
||||
|
||||
1. **IsAllowedGatewayTipVertex**: Returns `true` for left/right tips when the
|
||||
edge is a target entry (incoming).
|
||||
2. **HasValidGatewayBoundaryAngle**: Accepts any external approach angle at
|
||||
allowed tip vertices.
|
||||
3. **CountBoundarySlotViolations**: Skips slot-occupancy checks when all entries
|
||||
at a vertex share the same allowed tip point.
|
||||
|
||||
All three checks must stay synchronized -- changing one without the others causes
|
||||
cascading boundary-slot violations.
|
||||
|
||||
---
|
||||
|
||||
## Scoring System
|
||||
|
||||
### Violation Categories
|
||||
|
||||
The scoring system uses 18 violation categories with severity-weighted penalties.
|
||||
|
||||
#### Hard Violations (100,000 per instance)
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| Node crossings | Edge segment passes through a node bounding box |
|
||||
| Under-node | Edge runs beneath or through a node's vertical extent |
|
||||
| Shared lanes | Two edges share the same routing lane segment |
|
||||
| Boundary slots | More edges than slots on a node face |
|
||||
| Target joins | Multiple edges converge to the same target arrival point |
|
||||
| Gateway exits | Source exit from a blocked gateway vertex |
|
||||
| Collector corridors | Repeat-collector lane conflicts |
|
||||
| Below-graph | Edge segment routes below the graph's maximum Y extent |
|
||||
|
||||
#### Medium Violations (50,000 per instance)
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| Backtracking | Edge reverses direction in the target-approach window |
|
||||
| Detours | Unnecessary overshoot where a shorter path exists |
|
||||
|
||||
#### Soft Violations (200-650 per instance)
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| Edge crossings | Two edge segments intersect (200 per crossing) |
|
||||
| Proximity | Edge passes too close to a node boundary (400) |
|
||||
| Labels | Edge label overlaps another element (300) |
|
||||
| Bends | Excessive number of bend points (200 per extra bend) |
|
||||
| Diagonals | Non-orthogonal segment exceeds one node-shape length (650) |
|
||||
|
||||
### FinalScore Adjustments
|
||||
|
||||
The FinalScore applies detection exclusions that are NOT used during the iterative
|
||||
search. This separation is critical: the search uses the raw scoring as its
|
||||
heuristic, and changing it alters the search trajectory (causing speed regressions).
|
||||
|
||||
FinalScore excludes these borderline detection artifacts:
|
||||
|
||||
- **Valid gateway face approaches**: The exterior approach point is closer to
|
||||
the face center than the predecessor bend point (a legitimate face entry,
|
||||
not a violation).
|
||||
- **Gateway-exit under-node**: The lane runs within 16px of the source node's
|
||||
bottom boundary (a tight but valid exit, not a true under-node crossing).
|
||||
- **Convergent target joins from distant sources**: Sources separated by > 15px
|
||||
on the Y-axis with significant X separation (natural convergence, not a
|
||||
shared-lane conflict).
|
||||
- **Borderline shared lanes**: Gap between parallel edges is within 3px of
|
||||
the tolerance threshold (measurement noise, not a real overlap).
|
||||
|
||||
---
|
||||
|
||||
## Corridor Routing
|
||||
|
||||
Long-range edges that would cross many intermediate nodes are routed through
|
||||
corridors outside the main node field.
|
||||
|
||||
### Top Corridor (Long Sweeps)
|
||||
|
||||
For forward edges spanning more than 40% of the graph width:
|
||||
|
||||
- Route through the top corridor at `graphMinY - 56`.
|
||||
- Exit the source with a 24px perpendicular stub.
|
||||
- Route horizontally across the top corridor.
|
||||
- Descend to the target.
|
||||
|
||||
The 24px exit stub is critical: it prevents `NormalizeBoundaryAngles` from
|
||||
collapsing the vertical corridor segment back into the source boundary.
|
||||
|
||||
### Bottom Corridor (Near-Boundary Sweeps)
|
||||
|
||||
For edges that need to route near the graph's lower boundary:
|
||||
|
||||
- Route through the bottom corridor at `graphMaxY + 32`.
|
||||
- Same perpendicular exit stub pattern.
|
||||
|
||||
### Below-Graph Detection
|
||||
|
||||
The below-graph violation detector (`HasCorridorBendPoints`) exempts edges that
|
||||
intentionally use corridor routing. Without this exemption, corridor edges would
|
||||
be penalized as below-graph violations and rerouted back into the node field.
|
||||
|
||||
---
|
||||
|
||||
## Y-Gutter Expansion (Routing-Aware Placement Feedback Loop)
|
||||
|
||||
Y-gutter expansion is a post-routing placement correction that creates vertical
|
||||
routing space where the initial Sugiyama placement left insufficient clearance.
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Detection**: After edge routing, scan all routed edge segments for horizontal
|
||||
segments that violate under-node or alongside clearance rules.
|
||||
|
||||
2. **Identification**: For each violation, identify the blocking node and compute
|
||||
the required clearance:
|
||||
- Under-node: The edge passes through the node's bounding box.
|
||||
- Alongside (flush): The edge runs within +/-4px of a node's top or bottom
|
||||
boundary (the "alongside" extension catches edges "glued" to boundaries
|
||||
that the standard gap > 0.5px check misses).
|
||||
|
||||
3. **Expansion**: Shift ALL nodes below the violation Y downward by the computed
|
||||
clearance amount. This preserves relative ordering within each layer, unlike
|
||||
individual node shifting which disrupts Sugiyama optimization.
|
||||
|
||||
4. **Re-routing**: Re-route edges with the expanded corridors.
|
||||
|
||||
5. **Iteration**: Repeat up to 2 times to handle cascading violations (where
|
||||
fixing one violation exposes another).
|
||||
|
||||
### Design Rationale
|
||||
|
||||
The same pattern is used for X-gutter expansion (widening inter-layer gaps).
|
||||
Band-level shifting is fundamentally different from individual node adjustment:
|
||||
|
||||
- Individual node shifts break the barycenter ordering that the Sugiyama
|
||||
algorithm optimized, causing cascading position changes.
|
||||
- Post-refinement clearance insertion fails because subsequent optimization
|
||||
passes override the inserted space.
|
||||
- Band-level shifts preserve within-layer relationships while creating the
|
||||
needed routing corridors.
|
||||
|
||||
### Timing
|
||||
|
||||
Y-gutter expansion runs:
|
||||
- AFTER X-gutter expansion (inter-layer gaps are already set).
|
||||
- BEFORE compact passes (so compaction respects the new corridors).
|
||||
- BEFORE the iterative optimization loop (so the optimizer works with
|
||||
adequate routing space).
|
||||
|
||||
---
|
||||
|
||||
## Effort Levels
|
||||
|
||||
| Level | Ordering Iterations | Placement Iterations | Routing |
|
||||
|-------|--------------------:|---------------------:|---------|
|
||||
| Draft | 8 | 3 | Baseline only |
|
||||
| Balanced | 14 | 6 | Baseline + light repair |
|
||||
| Best | 24 | 10 | Hybrid deterministic with full-core parallel repair |
|
||||
|
||||
- **Draft**: Fastest layout for previews and interactive editing. No iterative
|
||||
optimization. Suitable for graphs under ~20 nodes.
|
||||
- **Balanced**: Good quality for medium graphs. Light repair fixes the worst
|
||||
violations without full A* search.
|
||||
- **Best**: Production quality for rendered artifacts (SVG, PNG). Full hybrid
|
||||
deterministic optimization with parallel candidate construction. Typical
|
||||
runtime: 12-15 seconds for complex workflow graphs.
|
||||
|
||||
---
|
||||
|
||||
## Layout Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Direction` | Enum | `LeftToRight` | Layout direction. `TopToBottom` uses the legacy iterative path. |
|
||||
| `NodeSpacing` | int | 40 | Vertical gap between nodes (px). Scaled by edge density up to 1.8x. |
|
||||
| `LayerSpacing` | int | 60 | Horizontal gap between layers (px). |
|
||||
| `Effort` | Enum | `Best` | Layout quality vs. speed tradeoff. |
|
||||
|
||||
### Edge Density Scaling
|
||||
|
||||
When the number of edges between two adjacent layers exceeds a threshold, both
|
||||
`NodeSpacing` and `LayerSpacing` are scaled up to accommodate additional routing
|
||||
channels. The maximum scale factor is 1.8x, applied per-layer-pair.
|
||||
|
||||
---
|
||||
|
||||
## Diagnostics
|
||||
|
||||
The layout engine emits detailed diagnostics when running in `Best` effort mode:
|
||||
|
||||
- **Live progress log**: Baseline state, strategy starts, per-attempt scores,
|
||||
and adaptation decisions logged during execution.
|
||||
- **Per-attempt phase timings**: Routing time, post-processing time, and
|
||||
route-pass counts for each optimization attempt.
|
||||
- **SVG/PNG/JSON artifacts**: The document-processing artifact test produces
|
||||
rendered output alongside diagnostic data.
|
||||
- **Violation reports**: Per-edge violation lists with category, severity,
|
||||
geometry details, and FinalScore adjustments.
|
||||
|
||||
Diagnostics are detailed enough to prove routing progress and to profile
|
||||
optimization performance for regression detection.
|
||||
Reference in New Issue
Block a user