Route

Route :: # (opaque)

Placement-independent routing for an already positioned, sized graph. layout produces deterministic axis-aligned polylines, honors sparse attachment rules, separates parallel edges into stable tracks, gives self-loops exterior paths, and returns one anchor for every sparse edge label in label input order. Every label anchor lies on its final edge path; labels may be centered on an edge or placed near either end, so relationship names, roles, and multiplicities remain visually attached without putting text or styling into the geometry API. Group attachments are exact boundary crossings: routes approach and leave them perpendicularly instead of following the group outline.

Sparse waypoint rules are exact: a routed edge passes through every listed point in order. Sparse guide rules are preferences supplied by placement; the router discards a guided candidate when it would lose clearance, reverse, self-intersect, or approach a node from the wrong direction. The router never moves or normalizes caller geometry; bounds.x and bounds.y report the actual drawing origin.

Nodes use their rectangular size as their boundary by default. A sparse boundary rule makes attachment points follow an ellipse inside the same sized box; routing still treats that box as the node's obstacle. Every ordinary route first travels outward from its selected attachment to the node's inflated routing boundary before it may turn. Edges sharing an exact attachment share that short terminal throat and then receive stable lanes. Refinement never moves these terminal corridors, so a route cannot return beneath an incident node after leaving its port. Exact waypoints remain obligations and can intentionally describe otherwise infeasible geometry; overlapping endpoint boxes may likewise leave no clear route.

Every edge that enters or leaves a group receives one perpendicular portal on that boundary. Sparse group attachments override those automatic portals. Automatic lets the router choose a side and a distinct position, On fixes only the side, and Fixed chooses an exact normalized offset. Flexible ports sharing a side are ordered from their endpoints and separated by edge_gap; when a short side cannot fit that gap, spacing compresses uniformly while preserving order. Nested portals are crossed in ancestry order and every resulting route is kept clear of obstacles.

A shared-end rule gives two or more edges one intentional junction at a node they have in common and selects the shared trunk's node attachment. Member edges must not repeat an attachment rule at that common end. Their index-aligned edge routes stop at the junction; shared_routes contains the single trunk to draw from the junction to the common node. This represents UML generalization and other converging relationships without drawing coincident independent edges.

default_settings : Settings

Readable clearance and lane separation, with four bounded joint sweeps.

layout : Input, Settings -> [Ok(Result), Err(List(Problem))]
Side : [Top, Right, Bottom, Left]
Attachment : [Automatic, On(Side), Fixed({ side : Side, offset : F64 })]
Group : { rect : Rect, parent : [Root, Parent(U64)] }
Input : { graph : { nodes : List({ width : F64, height : F64 }), edges : List({ from : U64, to : U64 }) }, positions : List({ x : F64, y : F64 }), groups : List(Group), memberships : List(Membership), attachments : List(AttachmentRule), group_attachments : List(GroupAttachmentRule), boundaries : List(BoundaryRule), edge_labels : List(EdgeLabel), shared_ends : List(SharedEndRule), waypoints : List(WaypointRule), guides : List(GuideRule) }
Settings : { obstacle_gap : F64, bend_penalty : F64, shared_path_penalty : F64, edge_gap : F64, max_sweeps : U64 }

obstacle_gap is the empty space kept around node and group boxes. edge_gap separates flexible ports and parallel lanes. bend_penalty favors fewer turns over a shorter candidate, while shared_path_penalty favors a distinct corridor. Zero disables the corresponding preference. max_sweeps bounds deterministic whole-drawing improvement passes; zero keeps the initial routes and still performs port and lane assignment.

SharedRoute : { edges : List(U64), endpoint : Endpoint, junction : Point, trunk : Route, group_crossings : List(GroupCrossing) }
Result : { layout : { positions : List(Point), routes : List(Route), bounds : Rect }, groups : List(Rect), label_anchors : List(Point), attachments : List(EdgeAttachments), group_crossings : List(List(GroupCrossing)), shared_routes : List(SharedRoute) }

Problem

:= [InvalidNodeWidth(U64), InvalidNodeHeight(U64), PositionCountMismatch, InvalidPosition(U64), InvalidEdgeFrom(U64), InvalidEdgeTo(U64), InvalidAttachmentEdge(U64), InvalidAttachmentOffset(U64), DuplicateAttachment(U64), InvalidGroupRect(U64), InvalidGroupParent(U64), InvalidMembershipNode(U64), InvalidMembershipGroup(U64), DuplicateMembership(U64), InvalidBoundaryNode(U64), DuplicateBoundary(U64), InvalidGroupAttachmentEdge(U64), InvalidGroupAttachmentGroup(U64), InvalidGroupAttachmentOffset(U64), DuplicateGroupAttachment(U64), GroupAttachmentNotBoundary(U64), InvalidLabelEdge(U64), InvalidLabelWidth(U64), InvalidLabelHeight(U64), InvalidWaypointEdge(U64), InvalidWaypoint(U64), DuplicateWaypoints(U64), BlockedWaypoint(U64), InvalidGuideEdge(U64), InvalidGuide(U64), DuplicateGuides(U64), SharedEndNeedsEdges(U64), InvalidSharedEndEdge(U64), DuplicateSharedEndEdge(U64), InvalidSharedEndAttachmentOffset(U64), SharedEndMismatch(U64), SharedEndMemberAttachment(U64), SharedEndOverlap(U64), InvalidObstacleGap, InvalidBendPenalty, InvalidSharedPathPenalty, InvalidEdgeGap]
to_str : Problem -> Str

Turn one typed problem into a short explanation for a person reading a log or error message. Numbers identify positions in the corresponding input list and start at zero.