Orthogonal Routing Guide

Comprehensive guide to automatic wire routing in kicad-sch-api using Manhattan-style (orthogonal) routing algorithms.

Table of Contents

Overview

The orthogonal routing module provides automatic Manhattan-style wire routing between points in KiCAD schematics. Instead of manually calculating wire paths and junction points, you can use the routing algorithm to generate L-shaped or direct wire routes.

What is Orthogonal Routing?

Orthogonal routing (also called Manhattan routing) creates wire paths that only use horizontal and vertical segments - never diagonal. This is the standard approach in electronic schematics and PCB design.

Point A ----β†’----+
                 |
                 ↓
                 Point B

Instead of: A β•± B (diagonal)

When to Use This

  • Automatic circuit generation: Let the algorithm handle wire routing

  • Component-to-component connections: Connect pins with proper L-shaped routes

  • Voltage dividers and T-junctions: Route with correct corner positions

  • MCP server integration: Enable AI agents to create properly routed circuits

Quick Start

Basic Usage

from kicad_sch_api.core.types import Point
from kicad_sch_api.geometry import create_orthogonal_routing

# Create routing between two points
result = create_orthogonal_routing(
    Point(100.0, 100.0),  # From position
    Point(150.0, 125.0)   # To position
)

print(f"Segments: {len(result.segments)}")  # 2 (L-shaped)
print(f"Corner: {result.corner}")           # Point(150.0, 100.0)
print(f"Is direct: {result.is_direct}")     # False

With Real Components

import kicad_sch_api as ksa
from kicad_sch_api.geometry import create_orthogonal_routing

# Create schematic and add components
sch = ksa.create_schematic("Auto Routing Demo")
r1 = sch.components.add("Device:R", "R1", "10k", position=(100.0, 100.0))
r2 = sch.components.add("Device:R", "R2", "10k", position=(150.0, 125.0))

# Get pin positions
r1_pins = sch.components.get_pins_info("R1")
r2_pins = sch.components.get_pins_info("R2")
r1_pin2 = next(p for p in r1_pins if p.number == "2")
r2_pin1 = next(p for p in r2_pins if p.number == "1")

# Create routing
result = create_orthogonal_routing(r1_pin2.position, r2_pin1.position)

# Add wires to schematic
for start, end in result.segments:
    sch.wires.add(start=start, end=end)

sch.save("auto_routed.kicad_sch")

Core Concepts

RoutingResult

The routing algorithm returns a RoutingResult object with three key attributes:

@dataclass
class RoutingResult:
    segments: List[Tuple[Point, Point]]  # Wire segments
    corner: Optional[Point]               # Corner junction point
    is_direct: bool                       # True if single straight line

Direct vs L-Shaped Routing

Direct Routing (when points are aligned):

# Horizontal alignment (same Y)
result = create_orthogonal_routing(
    Point(100.0, 100.0),
    Point(150.0, 100.0)
)
assert result.is_direct == True
assert len(result.segments) == 1
assert result.corner is None

L-Shaped Routing (when points are not aligned):

# Not aligned - needs corner
result = create_orthogonal_routing(
    Point(100.0, 100.0),
    Point(150.0, 125.0)
)
assert result.is_direct == False
assert len(result.segments) == 2
assert result.corner is not None  # Point(150.0, 100.0)

Segments

Each routing result contains a list of wire segments:

for start, end in result.segments:
    print(f"Wire from ({start.x}, {start.y}) to ({end.x}, {end.y})")

# Output for L-shaped routing:
# Wire from (100.0, 100.0) to (150.0, 100.0)  # Horizontal
# Wire from (150.0, 100.0) to (150.0, 125.0)  # Vertical

API Reference

create_orthogonal_routing()

def create_orthogonal_routing(
    from_pos: Point,
    to_pos: Point,
    corner_direction: CornerDirection = CornerDirection.AUTO
) -> RoutingResult:
    """
    Create orthogonal (Manhattan) routing between two points.

    Args:
        from_pos: Starting point
        to_pos: Ending point
        corner_direction: Direction preference for L-shaped corner
            - AUTO: Choose based on distance heuristic
            - HORIZONTAL_FIRST: Route horizontally, then vertically
            - VERTICAL_FIRST: Route vertically, then horizontally

    Returns:
        RoutingResult with segments list, corner point, and direct flag
    """

validate_routing_result()

def validate_routing_result(result: RoutingResult) -> bool:
    """
    Validate that routing result is correct.

    Checks:
    - All segments are orthogonal (horizontal or vertical)
    - Segments connect end-to-end
    - Corner point matches segment endpoints if present

    Raises:
        ValueError: If routing is invalid

    Returns:
        True if routing is valid
    """

Direction Modes

The routing algorithm supports three direction modes via the corner_direction parameter.

AUTO (Default)

Automatically chooses direction based on distance heuristic:

  • If dx >= dy: Route horizontally first

  • If dy > dx: Route vertically first

from kicad_sch_api.geometry import CornerDirection

# Horizontal distance (50) > vertical distance (25)
result = create_orthogonal_routing(
    Point(100.0, 100.0),
    Point(150.0, 125.0),
    corner_direction=CornerDirection.AUTO
)
# Result: Horizontal first β†’ corner at (150.0, 100.0)

When to use: Most cases - provides sensible routing automatically.

HORIZONTAL_FIRST

Always routes horizontally first, then vertically:

result = create_orthogonal_routing(
    Point(100.0, 100.0),
    Point(150.0, 125.0),
    corner_direction=CornerDirection.HORIZONTAL_FIRST
)
# Corner at (150.0, 100.0) - destination X, source Y

Visual:

Start ----β†’----+
               |
               ↓
               End

When to use:

  • Routing to power rails (horizontal buses)

  • Connecting to horizontal connectors

  • Aesthetic preference for horizontal-first routing

VERTICAL_FIRST

Always routes vertically first, then horizontally:

result = create_orthogonal_routing(
    Point(100.0, 100.0),
    Point(150.0, 125.0),
    corner_direction=CornerDirection.VERTICAL_FIRST
)
# Corner at (100.0, 125.0) - source X, destination Y

Visual:

Start
  |
  ↓
  +----β†’---- End

When to use:

  • Routing to ground planes (vertical connections)

  • Connecting to vertical connectors

  • Avoiding horizontal obstacles

Comparison Example

from_pos = Point(100.0, 100.0)
to_pos = Point(150.0, 125.0)

# All three modes
auto_result = create_orthogonal_routing(from_pos, to_pos, CornerDirection.AUTO)
h_first_result = create_orthogonal_routing(from_pos, to_pos, CornerDirection.HORIZONTAL_FIRST)
v_first_result = create_orthogonal_routing(from_pos, to_pos, CornerDirection.VERTICAL_FIRST)

print(f"AUTO corner: {auto_result.corner}")        # (150.0, 100.0) - horizontal first
print(f"H_FIRST corner: {h_first_result.corner}")  # (150.0, 100.0)
print(f"V_FIRST corner: {v_first_result.corner}")  # (100.0, 125.0)

KiCAD Y-Axis Inversion

Critical Concept

KiCAD uses an inverted Y-axis in schematic space. This is CRITICAL for understanding routing:

Normal (Math):        KiCAD (Graphics):
    +Y ↑                   +X β†’
       |                        ↓ +Y
  -----+----β†’ +X
       |

What This Means

  • Lower Y values = visually HIGHER on screen (top)

  • Higher Y values = visually LOWER on screen (bottom)

  • X-axis is normal (increases to the right)

Practical Example

# Component at top of screen
top_component = Point(100.0, 80.0)    # Lower Y = higher position

# Component at bottom of screen
bottom_component = Point(100.0, 120.0)  # Higher Y = lower position

# Routing "downward" on screen
result = create_orthogonal_routing(
    top_component,      # Y = 80 (visually higher)
    bottom_component    # Y = 120 (visually lower)
)
# Vertical segment goes from Y=80 to Y=120 (increasing Y = moving down)

Why This Matters

The routing algorithm handles this automatically, but you need to understand it when:

  • Interpreting pin positions

  • Debugging routing issues

  • Understanding corner positions

  • Reasoning about β€œabove” vs β€œbelow” in schematics

Testing Y-Axis Awareness

# Routing "upward" on screen (to lower Y)
result = create_orthogonal_routing(
    Point(100.0, 125.0),  # Start (visually lower)
    Point(150.0, 100.0),  # End (visually higher - lower Y!)
    corner_direction=CornerDirection.HORIZONTAL_FIRST
)

# Second segment should have decreasing Y (moving "up")
seg2_start, seg2_end = result.segments[1]
assert seg2_end.y < seg2_start.y  # End Y < Start Y means "upward"

Practical Examples

Example 1: Voltage Divider

import kicad_sch_api as ksa
from kicad_sch_api.geometry import create_orthogonal_routing

# Create voltage divider circuit
sch = ksa.create_schematic("Voltage Divider")

# Add resistors in series
r1 = sch.components.add("Device:R", "R1", "10k", position=(127.0, 88.9))
r2 = sch.components.add("Device:R", "R2", "10k", position=(127.0, 114.3))

# Get pin positions
r1_pins = sch.components.get_pins_info("R1")
r2_pins = sch.components.get_pins_info("R2")
r1_pin2 = next(p for p in r1_pins if p.number == "2")
r2_pin1 = next(p for p in r2_pins if p.number == "1")

# Route R1 to R2 (direct vertical - they're aligned)
result = create_orthogonal_routing(r1_pin2.position, r2_pin1.position)
for start, end in result.segments:
    sch.wires.add(start=start, end=end)

# Add output tap at midpoint
midpoint_y = (r1_pin2.position.y + r2_pin1.position.y) / 2
midpoint = Point(127.0, midpoint_y)
output = Point(160.0, midpoint_y)

result2 = create_orthogonal_routing(midpoint, output)
for start, end in result2.segments:
    sch.wires.add(start=start, end=end)

sch.save("voltage_divider.kicad_sch")

Example 2: Filter Chain

import kicad_sch_api as ksa
from kicad_sch_api.geometry import create_orthogonal_routing, CornerDirection

sch = ksa.create_schematic("Filter Chain")

# Add a chain of filters with alternating positions
filters = []
for i in range(5):
    x = 100.0 + i * 50.0
    y = 100.0 + (i % 2) * 25.0  # Zigzag pattern
    r = sch.components.add("Device:R", f"R{i+1}", "1k", position=(x, y))
    filters.append(r)

# Route between consecutive filters
for i in range(len(filters) - 1):
    r1_pins = sch.components.get_pins_info(filters[i].reference)
    r2_pins = sch.components.get_pins_info(filters[i+1].reference)

    r1_pin2 = next(p for p in r1_pins if p.number == "2")
    r2_pin1 = next(p for p in r2_pins if p.number == "1")

    # Use AUTO direction for natural routing
    result = create_orthogonal_routing(
        r1_pin2.position,
        r2_pin1.position,
        corner_direction=CornerDirection.AUTO
    )

    for start, end in result.segments:
        sch.wires.add(start=start, end=end)

sch.save("filter_chain.kicad_sch")

Example 3: Power Distribution

import kicad_sch_api as ksa
from kicad_sch_api.geometry import create_orthogonal_routing, CornerDirection

sch = ksa.create_schematic("Power Distribution")

# VCC rail position
vcc_rail = Point(50.0, 50.0)

# Add multiple components that need VCC
components = []
positions = [(100.0, 80.0), (150.0, 100.0), (120.0, 120.0)]
for i, pos in enumerate(positions):
    ic = sch.components.add(
        "Device:C",  # Using capacitors as example
        f"C{i+1}",
        "100nF",
        position=pos
    )
    components.append((ic, pos))

# Route VCC to each component
for ic, pos in components:
    # Use HORIZONTAL_FIRST to connect to horizontal VCC rail
    result = create_orthogonal_routing(
        vcc_rail,
        Point(pos[0], pos[1] - 10.0),  # Above component
        corner_direction=CornerDirection.HORIZONTAL_FIRST
    )

    for start, end in result.segments:
        sch.wires.add(start=start, end=end)

sch.save("power_distribution.kicad_sch")

Best Practices

1. Validate All Routing Results

Always validate routing results to catch errors early:

from kicad_sch_api.geometry import create_orthogonal_routing, validate_routing_result

result = create_orthogonal_routing(from_pos, to_pos)
validate_routing_result(result)  # Raises ValueError if invalid

2. Use AUTO Direction for General Routing

Unless you have a specific reason, use AUTO direction:

# Good - natural routing
result = create_orthogonal_routing(from_pos, to_pos)  # AUTO is default

# Only use specific directions when needed
result = create_orthogonal_routing(
    from_pos, to_pos,
    corner_direction=CornerDirection.HORIZONTAL_FIRST  # Specific requirement
)

3. Check for Direct Routing

Optimize by checking if routing is direct:

result = create_orthogonal_routing(from_pos, to_pos)

if result.is_direct:
    print("Simple connection - single wire")
else:
    print(f"L-shaped connection with corner at {result.corner}")

4. Grid Alignment

Ensure all positions are grid-aligned (1.27mm = 50mil):

from kicad_sch_api.core.geometry import snap_to_grid

# Snap positions to KiCAD grid before routing
from_pos = Point(*snap_to_grid((100.5, 100.3), grid_size=1.27))
to_pos = Point(*snap_to_grid((150.7, 125.2), grid_size=1.27))

result = create_orthogonal_routing(from_pos, to_pos)

5. Add Junction Markers

For L-shaped routing, mark the corner with a junction (future Phase 2 feature):

result = create_orthogonal_routing(from_pos, to_pos)

# Add wires
for start, end in result.segments:
    sch.wires.add(start=start, end=end)

# Mark corner (Phase 2 - junction API)
if result.corner:
    # Future: sch.junctions.add(position=result.corner)
    pass

6. Use Type Hints

Leverage type hints for better code quality:

from kicad_sch_api.core.types import Point
from kicad_sch_api.geometry import RoutingResult, create_orthogonal_routing

def route_component_pins(from_pin: Point, to_pin: Point) -> RoutingResult:
    """Route between two pin positions."""
    result = create_orthogonal_routing(from_pin, to_pin)
    validate_routing_result(result)
    return result

Integration with MCP Server

The routing functionality integrates with the MCP server for programmatic circuit generation. Here’s the planned integration (Phase 2):

MCP Tool: connect_components()

# Future MCP server tool
@server.call_tool()
async def connect_components(
    from_component: str,
    from_pin: str,
    to_component: str,
    to_pin: str,
    routing_style: str = "orthogonal",
    corner_direction: str = "auto"
) -> dict:
    """Connect component pins with automatic orthogonal routing."""

    # Get pin positions
    from_pos = get_pin_position(from_component, from_pin)
    to_pos = get_pin_position(to_component, to_pin)

    # Create routing
    direction = CornerDirection[corner_direction.upper()]
    result = create_orthogonal_routing(from_pos, to_pos, direction)

    # Add wires
    for start, end in result.segments:
        sch.wires.add(start=start, end=end)

    return {
        "success": True,
        "segments": len(result.segments),
        "corner": result.corner,
        "is_direct": result.is_direct
    }

Example Usage with AI

User: "Connect R1 pin 2 to R2 pin 1 with a label VCC"

AI: connect_components("R1", "2", "R2", "1", corner_direction="auto")

Troubleshooting

Problem: Diagonal Wire Segments

Symptom: ValueError: Segment is not orthogonal

Cause: Routing result contains diagonal (non-orthogonal) segments.

Solution: This should never happen with create_orthogonal_routing(). If it does, it’s a bug - please report it.

Problem: Disconnected Segments

Symptom: ValueError: Segments not connected

Cause: Segments don’t connect end-to-end.

Solution: Again, this is a bug if it happens. Validate your routing:

result = create_orthogonal_routing(from_pos, to_pos)
validate_routing_result(result)  # Will raise ValueError with details

Problem: Unexpected Corner Position

Symptom: Corner is not where you expect it.

Cause: Using wrong direction mode or misunderstanding Y-axis inversion.

Solution:

  1. Check which direction mode you’re using

  2. Remember: lower Y = visually higher on screen

  3. Try different direction modes:

# Try all modes to see the difference
for mode in [CornerDirection.AUTO, CornerDirection.HORIZONTAL_FIRST, CornerDirection.VERTICAL_FIRST]:
    result = create_orthogonal_routing(from_pos, to_pos, corner_direction=mode)
    print(f"{mode}: corner at {result.corner}")

Problem: Wire Doesn’t Appear in KiCAD

Symptom: Wire segments added but not visible in KiCAD.

Cause: Position mismatch or grid misalignment.

Solutions:

  1. Verify positions are in schematic coordinate space (mm)

  2. Check Y-axis inversion (lower Y = higher on screen)

  3. Ensure positions are grid-aligned (1.27mm increments)

  4. Save and reload schematic to verify

# Debug: Print wire positions
result = create_orthogonal_routing(from_pos, to_pos)
for i, (start, end) in enumerate(result.segments):
    print(f"Segment {i}: ({start.x:.2f}, {start.y:.2f}) β†’ ({end.x:.2f}, {end.y:.2f})")

Problem: Routing Through Obstacles

Symptom: Routed wire passes through other components.

Cause: Phase 1 has no collision detection.

Solution: This is expected behavior for Phase 1 (MVP). Collision detection is planned for Phase 2. Current workaround:

  • Manually adjust component positions

  • Use different direction modes to route around

  • Wait for Phase 2 waypoint routing

Further Reading

Contributing

Found a bug or have a feature request? Please open an issue on GitHub: https://github.com/circuit-synth/kicad-sch-api/issues


This feature was implemented in Phase 1 (MVP) - Issue #109