Why Orthogonal Wire Routing Is a Topology Guarantee, Not Just a Visual Preference
TLDR
Orthogonal (Manhattan) routing constrains every wire segment to horizontal or vertical. This looks like a display preference. It is actually a topology guarantee: connectivity can be determined with axis-aligned bbox intersection rather than general line-intersection math. Every downstream operation that needs to know what connects to what gets simpler.
The Problem Class
Diagram editors that support wire-based connectivity (circuit editors, flowcharts, node graphs) face a fundamental question at draw time: should wires be constrained to axis-aligned segments, or should users be able to draw arbitrary diagonal paths?
The choice feels like a UX decision. It is an architectural decision. The wire geometry you allow at draw time determines the complexity of every operation that happens after: topology analysis, connectivity queries, BOM generation, export, and layout algorithms.
The Naive Approach
Allow free-form paths. Let users draw wires at any angle. This is the most permissive design and feels most flexible. Users can position wires exactly where they want. The implementation is simple: record mouse points, draw a polyline, done.
This works until you need to answer the question: does this wire connect to this component?
A free-form polyline can approach a component's connection port from any angle. Determining whether it touches the port requires checking whether the polyline intersects a small target circle. That is a general computational geometry problem. For N wires and M ports it is O(N*M) intersection tests, each involving parametric line-segment math.
Why It Breaks
Free-form geometry creates compounding complexity. Every analysis operation that touches wire connectivity has to handle arbitrary angles. The snap algorithm has to compute distances to line segments rather than just grid positions. The highlight algorithm has to trace arbitrary polylines. The export algorithm has to output arbitrary path data rather than structured connection records.
More practically: free-form wires produce ambiguous connectivity. A wire that visually appears to touch a component port may miss by a fraction of a pixel due to floating-point precision in the draw loop. The topology analysis produces false negatives. The workaround is a generous snap threshold, which then produces false positives when wires are close but not connected.
Orthogonal routing eliminates the ambiguity at the source. If wires are constrained to horizontal and vertical segments, a wire connects to a port if and only if one of its endpoints falls within the port's bbox. Bbox intersection is exact. No parametric math, no threshold tuning.
The Better Model
Enforce the orthogonal constraint at the moment a wire segment is committed, not as a post-process. When the user moves the mouse, project the cursor to the nearest axis-aligned position from the last wire point:
function commitSegment(lastPt, cursorX, cursorY) {
const dx = Math.abs(cursorX - lastPt.x);
const dy = Math.abs(cursorY - lastPt.y);
// Lock to the dominant axis
return dx > dy
? { x: cursorX, y: lastPt.y }
: { x: lastPt.x, y: cursorY };
}
Every segment added to the wire is guaranteed to be either horizontal or vertical. The wire as a whole is a sequence of axis-aligned segments, which is a standard format for circuit diagram interchange (KiCad, gEDA, LTspice all use this).
Connectivity then reduces to endpoint snapping:
function isConnected(wireEndpoint, portBbox, epsilon) {
return (
wireEndpoint.x >= portBbox.x - epsilon &&
wireEndpoint.x <= portBbox.x + portBbox.width + epsilon &&
wireEndpoint.y >= portBbox.y - epsilon &&
wireEndpoint.y <= portBbox.y + portBbox.height + epsilon
);
}
One function, two comparisons per axis, works for every wire in the diagram regardless of routing complexity.
For bend points (where a wire changes direction from horizontal to vertical), store the sequence of points explicitly. Moving a wire endpoint updates only the adjacent segments:
function moveEndpoint(wire, endpointIndex, newX, newY) {
wire.points[endpointIndex] = { x: newX, y: newY };
// Re-project adjacent segment to maintain orthogonality
if (endpointIndex > 0) {
const prev = wire.points[endpointIndex - 1];
const dx = Math.abs(newX - prev.x);
const dy = Math.abs(newY - prev.y);
if (dx > dy) {
wire.points[endpointIndex - 1] = { x: prev.x, y: newY };
} else {
wire.points[endpointIndex - 1] = { x: newX, y: prev.y };
}
}
}
The constraint propagates through the wire on every edit. The wire is always valid orthogonal geometry.
Tradeoffs
Orthogonal routing forces wires to take L-shapes and Z-shapes rather than the shortest visual path. In dense diagrams this can produce longer wires that cross other elements. Auto-routing (finding an orthogonal path that avoids obstacles) is a separate problem and is much harder than constrained drawing.
For diagrams where users manually route wires, the constraint is acceptable because users naturally route orthogonally anyway. For diagrams that need auto-routing (large netlists, imported schematics), the constraint is necessary because auto-routing algorithms all produce orthogonal output.
The One Thing to Watch For
Orthogonal routing produces degenerate wires when an endpoint is moved to the same X or Y coordinate as an adjacent bend point. The resulting zero-length segment is valid geometry but invisible to the user. Track bend points explicitly and remove degenerate (zero-length) segments after every endpoint move.