Advanced Tutorial CodeΒΆ

from math import tau

import numpy as np

from kiva.api import CAP_ROUND, CIRCLE_MARKER, FILL, Font, STROKE
from kiva.image import GraphicsContext, CompiledPath


def draw_wire_with_components(gc, start, end, component_locations):
    """
    Draws a straight, axis aligned, wire with gaps in it for components. This
    function assumes the component locations are in order they are encountered
    when moving from start to end. 
    
    Parameters
    ----------
    gc : GraphicsContext
        The Graphics context doing the drawing
    start : 2-tuple
        The start point of the wire
    end : 2-tuple
        The end point of the wire
    component_locations : List of pairs of 2-tuple
        The start and end points of the components in order encountered
    """
    with gc:
        gc.set_stroke_color((0., 0., 0., 1.))
        gc.set_line_width(1.0)

        gc.move_to(*start)
        for comp_start, comp_end in component_locations:
            gc.line_to(*comp_start)
            gc.move_to(*comp_end)
        gc.line_to(*end)
        gc.stroke_path()


def draw_rect_wire_frame_with_components(gc, x, y, w, h, component_locations):
    """
    Draws an axis aligned rectangle of wire with gaps in it for components.
    component_locations is a list of pairs of points corresponding to the start
    and end of a component.  The function assumes the component locations are
    in order they are encountered when moving clockwise around the rectangle
    starting at the lower left corner.

    Parameters
    ----------
    gc : GraphicsContext
        The Graphics context doing the drawing
    x : int
        The left X coordinate of the rectangle
    y : int
        The bottom Y coordinate of the rectangle
    w : int
        The width of the rectangle
    h : int
        The height of the rectangle
    component_locations : List of pairs of 2-tuple
        The start and end points of the components in order encountered
    """
    left_comps = [
        comp_loc for comp_loc in component_locations if comp_loc[0][0] == x
    ]
    top_comps = [
        comp_loc for comp_loc in component_locations if comp_loc[0][1] == y + h
    ]
    right_comps =[
        comp_loc for comp_loc in component_locations if comp_loc[0][0] == x + w
    ]
    bottom_comps = [
        comp_loc for comp_loc in component_locations if comp_loc[0][1] == y
    ]

    draw_wire_with_components(gc, (x, y), (x, y + h), left_comps)
    draw_wire_with_components(gc, (x, y + h), (x + w, y + h), top_comps)
    draw_wire_with_components(gc, (x + w, y + h), (x + w, y), right_comps)
    draw_wire_with_components(gc, (x + w, y), (x, y), bottom_comps)


def draw_wire_connections_at_points(gc, points):
    """
    Draw wire connections at each of the given points. This function checks if
    the graphics context implements optimized methods for doing so, and draws
    using the most optimal approach available.

    Parameters
    ----------
    gc : GraphicsContext
        The Graphics context doing the drawing
    points : List of pairs of 2-tuple
        The points where wire connections are to be drawn
    """

    if hasattr(gc, 'draw_marker_at_points'):
        gc.draw_marker_at_points(points, 4.0, CIRCLE_MARKER)
    
    else:
        wire_connection_path = CompiledPath()
        wire_connection_path.move_to(0,0)
        wire_connection_path.arc(0, 0, 4, 0, tau)

        if hasattr(gc, 'draw_path_at_points'):
            gc.draw_path_at_points(points, wire_connection_path, FILL)
        else:
            for point in points:
                with gc:
                    gc.translate_ctm(point[0], point[1])
                    gc.add_path(wire_connection_path)
                    gc.fill_path()


def create_resistor_path():
    """
    Creates a CompiledPath for a resistor which can then be re-used as needed.

    Returns
    -------
    CompiledPath
        The reistor compiled path
    """
    resistor_path = CompiledPath()
    resistor_path.move_to(0,0)
    resistor_path_points = [(i*10+5, 10*(-1)**i) for i in range(8)]
    for x, y in resistor_path_points:
        resistor_path.line_to(x,y)
    resistor_path.line_to(80, 0)

    return resistor_path


def draw_resistors_at_points(gc, points, resistor_path):
    """
    Draw a resistor at each of the given points. This function checks if
    the graphics context implements an optimized method for doing so, and draws
    using the most optimal approach available.

    Parameters
    ----------
    gc : GraphicsContext
        The graphics context doing the drawing.
    points : List of pairs of 2-tuple
        The points where resistors are to be drawn 
    resistor_path : CompiledPath
        The resistor path we wish to draw
    """

    if hasattr(gc, 'draw_path_at_points'):
        gc.draw_path_at_points(points, resistor_path, STROKE)
    else:
        for point in points:
            with gc:
                gc.translate_ctm(point[0], point[1])
                gc.add_path(resistor_path)
                gc.stroke_path()


def draw_meter(gc, location, color, text):
    """
    Draws a meter of the given color, with the given text, at the given
    location.

    Parameters
    ----------
    gc : GraphicsContext
        The graphics context doing the drawing.
    location : 2-tuple
        The point where the meter is to be drawn 
    color : 3 or 4 component tuple (R, G, B[, A])
        The color of the meter
    text : str
        The text to be placed in the center of the meter symbol
    """
    font = Font('Times New Roman', size=20)
    with gc:
        gc.set_font(font)
        gc.set_fill_color(color)
        gc.set_line_width(3)
        gc.translate_ctm(*location)

        gc.arc(0, 0, 20, 0.0, tau)
        gc.draw_path()

        gc.set_fill_color((0., 0., 0., 1.0))
        x, y, w, h = gc.get_text_extent(text)
        gc.show_text_at_point(text, -w/2, -h/2)


def draw_switch(gc, location, angle):
    """
    Draws a switch at given location.  Assumes location is the connected side
    of the switch, and angle assumes orientation facing directly accross the
    switch.

    Parameters
    ----------
    gc : GraphicsContext
        The graphics context doing the drawing.
    location : 2-tuple
        The point where the switch is to be drawn 
    angle : float
        The angle of the switch
    """
    with gc:
        gc.translate_ctm(*location)
        gc.rotate_ctm(angle)
        gc.move_to(0, 0)
        gc.line_to(30, 0)
        gc.stroke_path()


def draw_battery(gc, location):
    """
    Draws a battery at given location. Battery will extend down from the given
    location.

    Parameters
    ----------
    gc : GraphicsContext
        The graphics context doing the drawing.
    location : 2-tuple
        The point where the switch is to be drawn 
    """
    with gc:
        gc.translate_ctm(*location)
        gc.move_to(0, 0)
        thin_starts = [(-20, 0), (-20, -18)]
        thin_ends = [(20,0), (20, -18)]
        gc.line_set(thin_starts, thin_ends)
        gc.stroke_path()
        thick_starts = [(-8, -10), (-8, -28)]
        thick_ends = [(8, -10), (8, -28)]
        gc.set_line_width(8)
        gc.set_line_cap(CAP_ROUND)
        gc.line_set(thick_starts, thick_ends)
        gc.stroke_path()


if __name__ == "__main__":

    gc = GraphicsContext((600, 300))

    # step 1) Draw a skeleton of the circuit
    component_locations = [
        ((260, 150), (340, 150)),
        ((550, 130), (550, 100)),
        ((550, 90), (550, 60)),
        ((430, 50), (350, 50)),
        ((230, 50), (150, 50))
    ]
    draw_rect_wire_frame_with_components(
        gc, 50, 50, 500, 100, component_locations
    )
    draw_rect_wire_frame_with_components(
        gc, 200, 200, 200, 50, [((340, 200), (260, 200))]
    )
    draw_wire_with_components(gc, (200, 150), (200, 200), [])
    draw_wire_with_components(gc, (400, 150), (400, 200), [])


    # step 2) draw dots for wire connections
    points = [(200, 150), (200, 200), (400, 150), (400, 200), (550, 130)]
    draw_wire_connections_at_points(gc, points)

    # step 3) Draw the meters
    draw_meter(gc, (50, 100), (.9, .9, 0.5, 1.0), 'A')  # Ammeter
    draw_meter(gc, (300, 250), (0.5, .9, 0.5, 1.0), 'V')  # Voltmeter

    #step 4) Draw the resistors
    resistor_path = create_resistor_path()
    resistor_locations = [(150, 50), (350, 50), (260, 150), (260, 200)]
    draw_resistors_at_points(gc, resistor_locations, resistor_path)

    # step 6) Draw the switch
    draw_switch(gc, (550,100), tau/6)

    # step 7) Draw the battery
    draw_battery(gc, (550,90))
    
    gc.save("images/tutorial_advanced.png")