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 firstIf
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:
Check which direction mode youβre using
Remember: lower Y = visually higher on screen
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:
Verify positions are in schematic coordinate space (mm)
Check Y-axis inversion (lower Y = higher on screen)
Ensure positions are grid-aligned (1.27mm increments)
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