Files
next-ai-draw-io/lib/diagram-engine/route.ts
dayuan.jiang 1e4c74d464 feat(diagram-engine): edge router — pinned ports, obstacle avoidance, cost search
Fixes the reported problem: arrows overlapped on top of icons and ran through shapes
they had nothing to do with.

The cause was a missing layer, not a bug. render.ts emitted only source and target, so
draw.io routed every edge itself — and its router sees the two terminals' bounds and
nothing else, not where the other icons are. It therefore ran lines straight through
whatever was in between and left several edges leaving one node at the same point.

Ported from drawio-ai-kit's router (MIT, see NOTICE):

  - Port de-collision. Edges leaving the same side of the same node spread along it,
    ordered by where their far end sits so they do not cross on the way out. An edge
    with a clean straight shot keeps the centre.
  - Obstacle avoidance. Candidate shapes in order of directness — straight, a Z through
    the gap, an L, a two-bend detour — each tested against every icon on the page.
  - Frame placement. A frame is passable (an edge into a VPC must cross its border) but
    not free: running alongside a border, or cutting through a frame that holds only one
    of the two endpoints, is penalised.
  - Port snapping. On a bent route, each port moves to the side its leg actually arrives
    from. Without this the terminal segment can pierce the icon to reach a far-side port.
  - Lane claiming. Each routed edge records the lanes it occupies so later edges avoid
    them, rather than colliding and being pulled apart afterwards.
  - Global nudge, three passes, reverting any move that makes a path worse.

Two things I got wrong on the way, both caught by looking at real geometry:

  - The Z corridor was computed as min/max of both nodes' edges, which spans the whole
    distance between them — including anything parked in between. So the lane sweep
    would place the detour's middle leg on top of the very icon it was avoiding. It has
    to be the gap: trailing edge of the first node to the leading edge of the other.
  - The router was fed layout's slot rectangles, but an icon's cell is the glyph square
    centred in a slot roughly twice as wide. Collision tests against slots both missed
    real overlaps and invented false ones. Extracted cellRect() so the router and the
    emitted XML cannot diverge.

Accept-or-reject was not enough on its own. When one endpoint is inside a VPC and the
other outside, EVERY path trespasses on that frame, so the strict rule always fails and
the relaxed pass took whatever it tried first — which is how a line ended up cutting
across a whole VPC. Candidates are now scored (frame offences 500, lane sharing 700,
bends 80, length 1) and the cheapest wins, so an edge that must trespass still gets the
least-bad route.

Waypoints are still written only when load-bearing — a labelled bend or a deliberate
detour — so an unobstructed edge stays drag-friendly.

455 unit tests (27 new, asserting produced geometry rather than algorithm shape).
Verified by rendering the reported diagram in the real editor: the two arrows that
overlapped on the EC2 icon now leave from different sides, and the load balancer's
fan-out leaves from three distinct points.
2026-08-09 13:49:11 +09:00

42 KiB