Skip to content

Layout

Diagram layout engine for positioning devices and routing wires.

layout

Layout engine for positioning components and routing wires.

LayoutConfig dataclass

LayoutConfig(
    board_margin_left: float = 40.0,
    board_margin_top: float = 40.0,
    device_area_left: float = 450.0,
    device_spacing_vertical: float = 20.0,
    device_margin_top: float = 60.0,
    rail_offset: float = 40.0,
    wire_spacing: float = 8.0,
    bundle_spacing: float = 4.0,
    corner_radius: float = 5.0,
    legend_margin: float = 20.0,
    legend_width: float = 150.0,
    legend_height: float = 120.0,
    pin_number_y_offset: float = 12.0,
    gpio_diagram_width: float = 125.0,
    gpio_diagram_margin: float = 40.0,
)

Configuration parameters for diagram layout.

Controls spacing, margins, and visual parameters for the diagram layout engine. All measurements are in SVG units (typically pixels).

ATTRIBUTE DESCRIPTION
board_margin_left

Left margin before board (default: 40.0)

TYPE: float

board_margin_top

Top margin before board (default: 40.0)

TYPE: float

device_area_left

X position where devices start (default: 450.0)

TYPE: float

device_spacing_vertical

Vertical space between stacked devices (default: 20.0)

TYPE: float

device_margin_top

Top margin for first device (default: 60.0)

TYPE: float

rail_offset

Horizontal distance from board to wire routing rail (default: 40.0)

TYPE: float

wire_spacing

Minimum vertical spacing between parallel wires (default: 8.0)

TYPE: float

bundle_spacing

Spacing between wire bundles (default: 4.0)

TYPE: float

corner_radius

Radius for wire corner rounding (default: 5.0)

TYPE: float

legend_margin

Margin around legend box (default: 20.0)

TYPE: float

legend_width

Width of legend box (default: 150.0)

TYPE: float

legend_height

Height of legend box (default: 120.0)

TYPE: float

pin_number_y_offset

Vertical offset for pin number circles (default: 12.0)

TYPE: float

gpio_diagram_width

Width of GPIO reference diagram (default: 125.0)

TYPE: float

gpio_diagram_margin

Margin around GPIO reference diagram (default: 40.0)

TYPE: float

LayoutEngine

LayoutEngine(config: LayoutConfig | None = None)

Calculate positions and wire routing for diagram components.

The layout engine handles the algorithmic placement of devices and routing of wires between board pins and device pins. It uses a "rail" system where wires route horizontally to a vertical rail, then along the rail, then horizontally to the device.

Wire routing features
  • Automatic offset for parallel wires from the same pin
  • Rounded corners for professional appearance
  • Multiple routing styles (orthogonal, curved, mixed)
  • Optimized path calculation to minimize overlaps

Initialize layout engine with optional configuration.

PARAMETER DESCRIPTION
config

Layout configuration parameters. If None, uses default LayoutConfig.

TYPE: LayoutConfig | None DEFAULT: None

Source code in src/pinviz/layout.py
def __init__(self, config: LayoutConfig | None = None):
    """
    Initialize layout engine with optional configuration.

    Args:
        config: Layout configuration parameters. If None, uses default LayoutConfig.
    """
    self.config = config or LayoutConfig()

layout_diagram

layout_diagram(
    diagram: Diagram,
) -> tuple[float, float, list[RoutedWire]]

Calculate layout for a complete diagram.

PARAMETER DESCRIPTION
diagram

The diagram to layout

TYPE: Diagram

RETURNS DESCRIPTION
tuple[float, float, list[RoutedWire]]

Tuple of (canvas_width, canvas_height, routed_wires)

Source code in src/pinviz/layout.py
def layout_diagram(self, diagram: Diagram) -> tuple[float, float, list[RoutedWire]]:
    """
    Calculate layout for a complete diagram.

    Args:
        diagram: The diagram to layout

    Returns:
        Tuple of (canvas_width, canvas_height, routed_wires)
    """
    # Position devices vertically on the right side
    self._position_devices(diagram.devices)

    # Route all wires
    routed_wires = self._route_wires(diagram)

    # Calculate canvas size
    canvas_width, canvas_height = self._calculate_canvas_size(diagram, routed_wires)

    return canvas_width, canvas_height, routed_wires

RoutedWire dataclass

RoutedWire(
    connection: Connection,
    path_points: list[Point],
    color: str,
    from_pin_pos: Point,
    to_pin_pos: Point,
)

A wire connection with calculated routing path.

Contains the complete routing information for a wire, including all waypoints along its path. This is the result of the layout engine's wire routing algorithm.

ATTRIBUTE DESCRIPTION
connection

The original connection specification

TYPE: Connection

path_points

List of points defining the wire path (min 2 points)

TYPE: list[Point]

color

Wire color as hex code (from connection or auto-assigned)

TYPE: str

from_pin_pos

Absolute position of source pin on board

TYPE: Point

to_pin_pos

Absolute position of destination pin on device

TYPE: Point

create_bezier_path

create_bezier_path(
    points: list[Point], corner_radius: float = 5.0
) -> str

Create an SVG path string with rounded corners.

PARAMETER DESCRIPTION
points

List of points defining the path

TYPE: list[Point]

corner_radius

Radius for rounded corners

TYPE: float DEFAULT: 5.0

RETURNS DESCRIPTION
str

SVG path d attribute string

Source code in src/pinviz/layout.py
def create_bezier_path(points: list[Point], corner_radius: float = 5.0) -> str:
    """
    Create an SVG path string with rounded corners.

    Args:
        points: List of points defining the path
        corner_radius: Radius for rounded corners

    Returns:
        SVG path d attribute string
    """
    if len(points) < 2:
        return ""

    # Start at first point
    path_parts = [f"M {points[0].x:.2f},{points[0].y:.2f}"]

    for i in range(1, len(points) - 1):
        prev = points[i - 1]
        curr = points[i]
        next_pt = points[i + 1]

        # Calculate direction vectors
        dx1 = curr.x - prev.x
        dy1 = curr.y - prev.y
        dx2 = next_pt.x - curr.x
        dy2 = next_pt.y - curr.y

        # Distance from corner point
        len1 = math.sqrt(dx1 * dx1 + dy1 * dy1)
        len2 = math.sqrt(dx2 * dx2 + dy2 * dy2)

        if len1 == 0 or len2 == 0:
            # Degenerate case, skip rounding
            path_parts.append(f"L {curr.x:.2f},{curr.y:.2f}")
            continue

        # Use smaller of corner_radius or half the segment length
        radius = min(corner_radius, len1 / 2, len2 / 2)

        # Calculate the points before and after the corner
        ratio1 = radius / len1
        ratio2 = radius / len2

        before = Point(curr.x - dx1 * ratio1, curr.y - dy1 * ratio1)
        after = Point(curr.x + dx2 * ratio2, curr.y + dy2 * ratio2)

        # Line to before corner, arc around corner
        path_parts.append(f"L {before.x:.2f},{before.y:.2f}")
        path_parts.append(f"Q {curr.x:.2f},{curr.y:.2f} {after.x:.2f},{after.y:.2f}")

    # Line to final point
    final = points[-1]
    path_parts.append(f"L {final.x:.2f},{final.y:.2f}")

    return " ".join(path_parts)