def get_r(self):
     if self._r is None:
         sx, sy = get_scale((self._x, self._y))
         rx = int(round(sx * from_mm(0.5))) # <-- desired global X radius
         ry = int(round(sy * from_mm(0.5))) # <-- desired global Y radius
         self._r = (rx, ry)
     return self._r
Exemple #2
0
 def coord_to_svg(coord):
     x, y = coord
     x -= cx
     y -= cy
     if swap_xy:
         x, y = y, x
     if flipped:
         x = -x
     y = -y
     x += w_total / 2 + from_mm(50) + drawing_w
     y += h_total / 2 + from_mm(400)
     return to_svg(x), to_svg(y)
 def get_scale(coord, r=from_mm(1)):
     a = glob((coord[0] + r, coord[1]))
     b = glob((coord[0] - r, coord[1]))
     x = 2*r / math.hypot(a[0] - b[0], a[1] - b[1])
     a = glob((coord[0], coord[1] + r))
     b = glob((coord[0], coord[1] - r))
     y = 2*r / math.hypot(a[0] - b[0], a[1] - b[1])
     return (x, y)
Exemple #4
0
    def to_file(self, fname):
        """Output the acylic plate as gerber files. This is f*****g weird, I
        know, and requires a circular module dependency because acrylic plates
        were also shoehorned into CircuitBoard, but this is the easiest path
        that I have right now for getting them rendered as SVGs and 3D
        models."""
        from circuit_board import GerberLayer

        cuts = GerberLayer('GM1')
        for path in self._cuts:
            cuts.add_path(from_mm(0.3), *path)
        cuts.to_file(fname)

        engravings = GerberLayer('GM2')
        for path in self._lines:
            engravings.add_path(from_mm(0.3), *path)
        for path in self._regions:
            engravings.add_region(True, *path)
        engravings.to_file(fname)
Exemple #5
0
    def to_file(self, fname):
        name = os.environ.get('ACRYLIC_NAME', '...')
        email = os.environ.get('ACRYLIC_EMAIL', '...')
        telephone = os.environ.get('ACRYLIC_PHONE', '...')
        skip = os.environ.get('ACRYLIC_SKIP', '')
        if skip:
            skip = set(skip.split(':'))
        else:
            skip = set()

        cutting_layer = []
        engrave_line_layer = []
        engrave_region_layer = []
        outline_layer = []
        notes_layer = []

        drawing_w = 0
        drawing_h = 0

        def to_svg(x):
            # 90 DPI for some reason
            return to_mm(x) * 90 / 25.4

        ident_counter = [0]

        def ident(prefix=''):
            ident_counter[0] += 1
            return '{}{}'.format(prefix, ident_counter[0])

        index = 1
        for (plate_name, plate) in self._plates.items():
            plate.to_file('{}.{}'.format(fname, plate_name))

            if plate_name in skip:
                print('SKIPPING plate {} due to environment variables'.format(
                    plate_name))
                continue

            sizes = list(map(from_mm, [200, 300, 450, 600, 900]))
            x_min, x_max, y_min, y_max = plate.get_bounds()
            w = x_max - x_min
            h = y_max - y_min
            if w == 0 or h == 0:
                continue
            cx = (x_max + x_min) / 2
            cy = (y_max + y_min) / 2
            flipped = plate.is_flipped()
            for idx, (w_total,
                      h_total) in enumerate(zip(sizes[1:], sizes[:-1])):
                if w < w_total - from_mm(10) and h < h_total - from_mm(10):
                    swap_xy = False
                    break
                if h < w_total - from_mm(10) and w < h_total - from_mm(10):
                    swap_xy = True
                    flipped = not flipped
                    break
            else:
                assert False

            print('{} design is {:.1f}x{:.1f} mm, need {:.0f}x{:.0f} plate'.
                  format(plate_name, to_mm(w), to_mm(h), to_mm(w_total),
                         to_mm(h_total)))

            cx -= max((w_total - w) / 2 - from_mm(20), 0)
            cy -= max((h_total - h) / 2 - from_mm(20), 0)

            def coord_to_svg(coord):
                x, y = coord
                x -= cx
                y -= cy
                if swap_xy:
                    x, y = y, x
                if flipped:
                    x = -x
                y = -y
                x += w_total / 2 + from_mm(50) + drawing_w
                y += h_total / 2 + from_mm(400)
                return to_svg(x), to_svg(y)

            def svg_path(path):
                closed = path[0] == path[-1] and len(path) > 2
                if closed:
                    path = path[:-1]
                data = []
                for coord in path:
                    if not data:
                        data.append('M')
                    else:
                        data.append('L')
                    data.append('{},{}'.format(*coord_to_svg(coord)))
                if closed:
                    data.append('Z')
                return ' '.join(data)

            for path in plate.iter_cuts():
                cutting_layer.append((
                    '<path inkscape:connector-curvature="0" id="{}" ' +
                    'style="display:inline;fill:none;stroke:#ff0000;' +
                    'stroke-width:{};stroke-linecap:square;stroke-linejoin:miter;'
                    +
                    'stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" '
                    + 'd="{}" />').format(ident('path'), to_svg(from_mm(0.05)),
                                          svg_path(path)))

            for path in plate.iter_lines():
                engrave_line_layer.append((
                    '<path inkscape:connector-curvature="0" id="{}" ' +
                    'style="display:inline;fill:none;stroke:#0000ff;' +
                    'stroke-width:{};stroke-linecap:square;stroke-linejoin:miter;'
                    +
                    'stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" '
                    + 'd="{}" />').format(ident('path'), to_svg(from_mm(0.05)),
                                          svg_path(path)))

            for path in plate.iter_regions():
                engrave_region_layer.append((
                    '<path inkscape:connector-curvature="0" id="{}" ' +
                    'style="fill:#000000;fill-opacity:1;stroke:none;' +
                    'stroke-width:0.5;stroke-linecap:square;stroke-linejoin:miter;'
                    +
                    'stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" '
                    + 'd="{}" />').format(ident('path'), svg_path(path)))

            outline_layer.append(
                ('<rect id="{}" x="{}" y="{}" width="{}" height="{}" ' +
                 'style="display:inline;opacity:1;fill:none;stroke:#00ff00;' +
                 'stroke-width:{};stroke-miterlimit:4;stroke-dasharray:none;' +
                 'stroke-dashoffset:0;stroke-opacity:1" />').format(
                     ident('rect'), to_svg(from_mm(50) + drawing_w),
                     to_svg(from_mm(400)), to_svg(w_total), to_svg(h_total),
                     to_svg(from_mm(1))))

            notes_layer.append((
                '<rect id="{}" x="{}" y="{}" width="{}" height="{}" ' +
                'style="display:inline;fill:none;fill-rule:evenodd;stroke:#000000;'
                +
                'stroke-width:{};stroke-linecap:butt;stroke-linejoin:miter;' +
                'stroke-miterlimit:4;stroke-dasharray:14.17885439, 28.35770877999999939;'
                + 'stroke-dashoffset:0;stroke-opacity:1;" />').format(
                    ident('rect'), to_svg(from_mm(55) + drawing_w),
                    to_svg(from_mm(405)), to_svg(w_total - from_mm(10)),
                    to_svg(h_total - from_mm(10)), to_svg(from_mm(1))))

            notes_layer.append("""<flowRoot
                transform="translate({},{})"
                style="font-style:normal;font-weight:normal;line-height:0.01%;font-family:roboto;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;-inkscape-font-specification:roboto;font-stretch:normal;font-variant:normal;"
                id="flowRoot{}"
                xml:space="preserve"><flowRegion
                style="-inkscape-font-specification:roboto;font-family:roboto;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;"
                id="flowRegion{}"><rect
                    style="font-size:68.75px;-inkscape-font-specification:roboto;font-family:roboto;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;"
                    y="-1000.7537"
                    x="-265.16504"
                    height="1199.8718"
                    width="2079.3359"
                    id="rect{}" /></flowRegion><flowPara
                style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.732px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;"
                id="flowPara{}">PLAAT#: {:02d}</flowPara><flowPara
                id="flowPara{}"
                style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:88.5827px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">{}X{}mm</flowPara><flowPara
                id="flowPara{}"
                style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:88.5827px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;"> </flowPara><flowPara
                id="flowPara{}"
                style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:88.5827px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">MATERIAAL: {}</flowPara><flowPara
                id="flowPara{}"
                style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:88.5827px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">DIKTE: {}</flowPara></flowRoot>"""
                               .format(to_svg(from_mm(125) + drawing_w),
                                       to_svg(from_mm(500)), ident(), ident(),
                                       ident(), ident(), index, ident(),
                                       int(round(to_mm(w_total))),
                                       int(round(to_mm(h_total))), ident(),
                                       ident(), plate.get_material(), ident(),
                                       plate.get_thickness()))

            drawing_w += w_total + from_mm(300)
            drawing_h = max(drawing_h, h_total + from_mm(450))
            index += 1

        if drawing_w == 0:
            if os.path.isfile('{}.laserbeest.svg'.format(fname)):
                os.unlink('{}.laserbeest.svg'.format(fname))
            if os.path.isfile('{}.laserbeest.pdf'.format(fname)):
                os.unlink('{}.laserbeest.pdf'.format(fname))
            return

        svg = []
        svg.append((
            '<svg xmlns:dc="http://purl.org/dc/elements/1.1/" ' +
            'xmlns:cc="http://creativecommons.org/ns#" ' +
            'xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ' +
            'xmlns:svg="http://www.w3.org/2000/svg" ' +
            'xmlns="http://www.w3.org/2000/svg" ' +
            'xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" '
            + 'xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" ' +
            'sodipodi:docname="Inkscape-Template-V2-0-NL.svg" ' +
            'inkscape:version="1.0 (4035a4fb49, 2020-05-01)" ' +
            'viewBox="0 0 {} {}" width="{}mm" height="{}mm">').format(
                to_svg(drawing_w), to_svg(drawing_h), to_mm(drawing_w),
                to_mm(drawing_h)))
        svg.append(
            ('<sodipodi:namedview  inkscape:document-rotation="0" ' +
             'inkscape:window-maximized="1" inkscape:window-y="-8" ' +
             'inkscape:window-x="-8" inkscape:window-height="987" ' +
             'inkscape:window-width="1680" inkscape:snap-global="false" ' +
             'inkscape:guide-bbox="true" showguides="true" showgrid="false" ' +
             'inkscape:current-layer="layer1" inkscape:document-units="mm" ' +
             'inkscape:cy="{}" inkscape:cx="{}" inkscape:zoom="1" ' +
             'inkscape:pageshadow="2" inkscape:pageopacity="0.0" ' +
             'borderopacity="1.0" bordercolor="#666666" pagecolor="#ffffff" ' +
             'id="base" />').format(to_svg(drawing_w / 2),
                                    to_svg(drawing_h / 2)))
        svg.append(
            '<g style="display:inline" inkscape:label="01_SNIJLIJNEN" id="layer1" inkscape:groupmode="layer">'
        )
        svg.extend(cutting_layer)
        svg.append('</g>')
        svg.append(
            '<g style="display:inline" inkscape:label="02_GRAVEERLIJNEN" id="layer2" inkscape:groupmode="layer">'
        )
        svg.extend(engrave_line_layer)
        svg.append('</g>')
        svg.append(
            '<g style="display:inline" inkscape:label="03_GRAVEERVLAKKEN" id="layer3" inkscape:groupmode="layer">'
        )
        svg.extend(engrave_region_layer)
        svg.append('</g>')
        svg.append(
            '<g style="display:inline" inkscape:label="04_WERKBLAD" id="layer4" inkscape:groupmode="layer">'
        )
        svg.extend(outline_layer)
        svg.append('</g>')
        svg.append(
            '<g style="display:inline" inkscape:label="05_TEKSTEN_INSTRUCTIES" id="layer5" inkscape:groupmode="layer">'
        )
        svg.extend(notes_layer)
        svg.append("""<flowRoot
          transform="translate({},{})"
          style="font-style:normal;font-weight:normal;line-height:0.01%;font-family:roboto;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;-inkscape-font-specification:roboto;font-stretch:normal;font-variant:normal;"
          id="flowRoot{}"
          xml:space="preserve"><flowRegion
            style="-inkscape-font-specification:roboto;font-family:roboto;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;"
            id="flowRegion{}"><rect
              style="font-size:68.75px;-inkscape-font-specification:roboto;font-family:roboto;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;"
              y="-1000.7537"
              x="-265.16504"
              height="1199.8718"
              width="2079.3359"
              id="rect{}" /></flowRegion><flowPara
            id="flowPara{}"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.732px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">NAAM: {}</flowPara><flowPara
            id="flowPara{}"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.732px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">E-MAIL: {}</flowPara><flowPara
            id="flowPara{}"
            style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:141.732px;line-height:1.25;font-family:roboto;-inkscape-font-specification:roboto;">TELEFOON: {}</flowPara></flowRoot>"""
                   .format(to_svg(from_mm(125)), to_svg(from_mm(300)), ident(),
                           ident(), ident(), ident(), name, ident(), email,
                           ident(), telephone))
        svg.append('</g>')
        svg.append('</svg>')

        with open('{}.laserbeest.svg'.format(fname), 'w') as f:
            f.write('\n'.join(svg) + '\n')

        subprocess.run([
            'inkscape', '-C', '-A', '{}.laserbeest.pdf'.format(fname),
            '{}.laserbeest.svg'.format(fname)
        ],
                       check=True)
Exemple #6
0
        with open('{}.laserbeest.svg'.format(fname), 'w') as f:
            f.write('\n'.join(svg) + '\n')

        subprocess.run([
            'inkscape', '-C', '-A', '{}.laserbeest.pdf'.format(fname),
            '{}.laserbeest.svg'.format(fname)
        ],
                       check=True)


if __name__ == '__main__':
    import math
    plates = LaseredAcrylic()
    plate = plates.add('driehoek', 'kek', 'banaan')
    plate.add_cut(*((from_mm(100 * math.sin(x / 120 * math.pi)),
                     from_mm(200 * math.cos(x / 120 * math.pi)))
                    for x in range(241)))
    plate.add_line(*((from_mm(50 * math.sin(x / 120 * math.pi)),
                      from_mm(50 * math.cos(x / 120 * math.pi)))
                     for x in range(241)))
    plate.add_region(*((from_mm(25 * math.sin(x / 120 * math.pi) + 25),
                        from_mm(25 * math.cos(x / 120 * math.pi) + 25))
                       for x in range(241)))
    plate = plates.add('driehoek2', 'kek2', 'banaan2')
    plate.add_line(*((from_mm(50 * math.sin(x / 120 * math.pi)),
                      from_mm(50 * math.cos(x / 120 * math.pi)))
                     for x in range(241)))
    plate.add_region(*((from_mm(25 * math.sin(x / 120 * math.pi) + 25),
                        from_mm(25 * math.cos(x / 120 * math.pi) + 25))
                       for x in range(241)))
from coordinates import LinearTransformer, CircularTransformer
from circuit_board import CircuitBoard
from subcircuit import get_subcircuit
from primitive import get_primitive
from coordinates import from_mm, to_mm
import gerbertools
import math
import sys

mainboard = CircuitBoard(mask_expansion=0.05)
t = LinearTransformer()
get_primitive('mainboard').instantiate(mainboard, t, (0, 0), 0, '', {})
t = CircularTransformer((0, 0), from_mm(159.15), 0)
get_subcircuit('mainboard').instantiate(mainboard, t,
                                        (from_mm(500), from_mm(0.85)),
                                        math.pi / 2, '', {})

print('*** pouring inner layer polygons...')
mainboard.add_poly_pours()

print('*** writing gerber output...')
mainboard.to_file('output/mainboard')

print('*** running circuit DRC...')
any_violations = False
if not mainboard.get_netlist().check_composite():
    any_violations = True

print('*** building light barrier guide...')
barrier_gbr = gerbertools.CircuitBoard('output/mainboard.PCB', '.GM1', '')
barrier_gbr.add_copper_layer('.LB', 2.5)
Exemple #8
0
    def instantiate(self, pcb, transformer, translate, rotate):

        # Determine scale.
        ref_coord = transrot(self._translate, translate, rotate)
        ref_rot = rotate + self._rotate
        scale_x, scale_y = transformer.get_scale(ref_coord, ref_rot)
        scale_x = self._scale * 0.1 / scale_x
        scale_y = self._scale * 0.1 / scale_y

        # Determine whether the text needs to be flipped to be readable.
        _, angle = transformer.part_to_global((0, 0), 0, ref_coord, ref_rot)
        angle += 0.5 * math.pi
        while angle >= 2*math.pi:
            angle -= 2*math.pi
        while angle < 0:
            angle += 2*math.pi
        flip_x = -1 if angle > math.pi else 1
        flip_y = -1 if angle > math.pi else 1

        # Render an overbar if the text ends in a backslash, sort of like
        # Altium (except not on character-basis).
        overbar = self._text.endswith('\\')
        if overbar:
            text = self._text[:-1]
        else:
            text = self._text

        # Abuse matplotlib to render some text.
        fp = FontProperties(self._family, self._style, weight=self._weight)
        path = TextPath((0, 0), text, 12, prop=fp)
        polys = [[tuple(x) for x in poly] for poly in path.to_polygons()]

        # Determine extents.
        x_min = 0
        y_min = 0
        x_max = 0
        y_max = 0
        for poly in polys:
            for coord in poly:
                x_min = min(x_min, coord[0])
                x_max = max(x_max, coord[0])
                #y_min = min(y_min, coord[1])
                #y_max = max(y_max, coord[1])
        for poly in TextPath((0, 0), 'jf', 12, prop=fp).to_polygons():
            for _, y in poly:
                y_min = min(y_min, y)
                y_max = max(y_max, y)

        # Render the overbar.
        if overbar:
            polys.append([
                (x_min, y_max + 1.5),
                (x_min, y_max + 3),
                (x_max, y_max + 3),
                (x_max, y_max + 1.5),
                (x_min, y_max + 1.5)
            ])
            y_max += 3

        # Flip if needed.
        for poly in polys:
            for i in range(len(poly)):
                poly[i] = (poly[i][0] * flip_x, poly[i][1] * flip_y)
        x_min *= flip_x
        x_max *= flip_x
        y_min *= flip_y
        y_max *= flip_y

        # Shift based on alignment and apply transformation.
        ox = (x_min + (x_max - x_min) * (self._halign if flip_x > 0 else 1.0 - self._halign)) if self._halign is not None else 0
        oy = (y_min + (y_max - y_min) * (self._valign if flip_y > 0 else 1.0 - self._valign)) if self._valign is not None else 0
        for poly in polys:
            for i in range(len(poly)):
                poly[i] = transrot((
                    from_mm((poly[i][0] - ox) * scale_x),
                    from_mm((poly[i][1] - oy) * scale_y),
                ), self._translate, self._rotate)

        # Determine winding order to detect whether to render as dark or clear.
        dark = []
        clear = []
        for poly in polys:
            poly = [tuple(x) for x in poly]
            winding = 0
            for (x1, y1), (x2, y2) in zip(poly[1:], poly[:-1]):
                winding += (x2 - x1) * (y2 + y1)
            if winding < 0:
                dark.append(poly)
            else:
                clear.append(poly)

        # Add the paths to the PCB.
        for path in dark:
            path = transformer.path_to_global(path, translate, rotate, True)
            pcb.add_region(self._layer, True, *path)
        for path in clear:
            path = transformer.path_to_global(path, translate, rotate, True)
            pcb.add_region(self._layer, False, *path)
    def instantiate(self, pcb, transformer, translate, rotate, net_prefix, net_override):
        """Instantiates this subcircuit on the given PCB with the given
        transformer and local coordinate + rotation. Nets found in the
        net_override map will be renamed accordingly. Local nets not found
        in the map will be prefixed by net_prefix."""

        # Rebuild netlist with the correct transformations.
        netlist = Netlist()
        for net, layer, coord, trans, rot, mode in self._net_insns:
            trans = transrot(trans, translate, rotate)
            rot += rotate
            netlist.add(net, layer, transformer.to_global(coord, trans, rot, False), mode)

        def translate_net(name):
            name = name.split('~')[0].split('*')[0]
            if name in net_override:
                name = net_override[name].split('~')[0].split('*')[0]
            elif name.startswith('.'):
                name = net_prefix + name
            return name

        # Handle primitive artwork and netlist.
        for instance in self._instances:
            instance.instantiate(pcb, transformer, translate, rotate, net_prefix, net_override)
        for net in netlist.iter_physical():
            name = translate_net(net.get_name())
            for layer, coord, mode in net.iter_points():
                pcb.add_net(name, layer, coord, mode)

        # Handle net ties.
        for master, slave in self._net_ties:
            pcb.add_net_tie(translate_net(master), translate_net(slave))

        # Perform routing.
        routers = [RoutingColumn(x, net, 'GTL', 'GBL') for x, nets in self._routers for net in nets]
        x_coords = {net: x for x, nets in self._routers for net in nets}
        for net in netlist.iter_logical():
            name = net.get_name()
            router_x = x_coords.get(name, None)
            if router_x is None:
                continue
            for layer, coord, _ in net.iter_points():
                coord = transformer.to_local(coord, translate)
                coord = transrot(coord, (-translate[0], -translate[1]), 0.0)
                coord = transrot(coord, (0, 0), -rotate)
                for router in routers:
                    router.register(name, router_x, coord, layer)
        for router in routers:
            router.generate(pcb, transformer, translate, rotate)
        for (x1, y1), (x2, y2), layer in self._shunters:
            path = []
            for i in range(41):
                fw = i / 40
                fy = 0.5 - math.cos(fw * math.pi) / 2
                fx = 0.5 - math.cos(fy * math.pi) / 2
                path.append((
                    int(round(x1 + fx * (x2 - x1))),
                    int(round(y1 + fy * (y2 - y1)))
                ))
            path = transformer.path_to_global(path, translate, rotate, True)
            pcb.add_trace('GTO', from_mm(0.2), *path)
            pcb.add_trace(layer, from_mm(0.2), *path)

        # Add labels.
        for label in self._labels:
            label.instantiate(pcb, transformer, translate, rotate)

        # Add outlines.
        for x1, y2, x2, y1, expand, corner, plate in self._outlines:
            plates = pcb.get_plates()
            if not plates.has_plate(plate):
                continue
            plate = plates.get(plate)

            x1 -= expand
            y1 -= expand
            x2 += expand
            y2 += expand

            def compute_corner_radius(x, y):
                scale_x, scale_y = transformer.get_scale((x, y))
                return int(round(corner / scale_x)), int(round(corner / scale_y))

            path = []
            def add(theta, x, y, corner_x, corner_y):
                path.append((
                    int(round(math.cos(theta) * corner_x)) + x,
                    int(round(math.sin(theta) * corner_y)) + y
                ))

            x = x2
            y = y2
            corner_x, corner_y = compute_corner_radius(x, y)
            center_x = x - corner_x
            center_y = y - corner_y
            for i in range(101):
                add(i / 50 * math.pi, center_x, center_y, corner_x, corner_y)
                if i == 25:
                    x = x1
                    corner_x, corner_y = compute_corner_radius(x, y)
                    center_x = x + corner_x
                    center_y = y - corner_y
                    add(i / 50 * math.pi, center_x, center_y, corner_x, corner_y)
                elif i == 50:
                    y = y1
                    corner_x, corner_y = compute_corner_radius(x, y)
                    center_x = x + corner_x
                    center_y = y + corner_y
                    add(i / 50 * math.pi, center_x, center_y, corner_x, corner_y)
                elif i == 75:
                    x = x2
                    corner_x, corner_y = compute_corner_radius(x, y)
                    center_x = x - corner_x
                    center_y = y + corner_y
                    add(i / 50 * math.pi, center_x, center_y, corner_x, corner_y)
                elif i == 100:
                    y = y2
                    corner_x, corner_y = compute_corner_radius(x, y)
                    center_x = x - corner_x
                    center_y = y - corner_y
                    add(i / 50 * math.pi, center_x, center_y, corner_x, corner_y)
            assert path[0] == path[-1]
            plate.add_cut(*transformer.path_to_global(path, translate, rotate, True))
Exemple #10
0
    def __init__(self, name):
        super().__init__()
        print('loading subcircuit {}...'.format(name))
        if not os.path.isdir(os.path.join('subcircuits', name)):
            raise ValueError('subcircuit {} does not exist'.format(name))
        if not os.path.isfile(os.path.join('subcircuits', name, '{}.circuit.txt'.format(name))):
            raise ValueError('missing .circuit.txt file for subcircuit {}'.format(name))
        cols = GridDimension(False)
        rows = GridDimension(True)
        self._name = name
        self._pins = Pins()
        self._interfaces = []
        self._net_insns = []
        self._net_ties = []
        self._instances = []
        self._routers = []
        self._shunters = []
        self._outlines = []
        self._labels = []
        instance_names = set()
        forwarded_pins = []
        with open(os.path.join('subcircuits', name, '{}.circuit.txt'.format(name)), 'r') as f:
            for line in f.read().split('\n'):
                line = line.split('#', maxsplit=1)[0].strip()
                if not line:
                    continue
                args = line.split()

                if args[0] == 'columns':
                    cols.configure(*args[1:])
                    continue

                if args[0] == 'rows':
                    rows.configure(*args[1:])
                    continue

                if args[0] in ('in', 'out'):
                    coord = (cols.convert(args[3]), rows.convert(args[4]))
                    self._pins.add(args[1], args[0], args[2], (0, 0), coord, 0.0)
                    name = '.{}'.format(args[1])
                    self._net_insns.append((name, args[2], (0, 0), coord, 0.0, args[0]))
                    continue

                if args[0] in ('fwd_in', 'fwd_out'):
                    forwarded_pins.append((args[0][4:], args[1]))
                    continue

                if args[0] in ('prim', 'subc'):
                    coord = (cols.convert(args[4]), rows.convert(args[5]))
                    rotation = float(args[3]) / 180 * math.pi
                    instance_type = args[1]
                    instance_name = args[2]
                    if instance_name in instance_names:
                        raise ValueError('duplicate instance name {}'.format(instance_name))
                    instance_names.add(instance_name)
                    if args[0] == 'prim' and instance_type == 'tie':
                        master = args[-1].split('=', maxsplit=1)[1]
                        for arg in args[6:-1]:
                            slave = arg.split('=', maxsplit=1)[1]
                            self._net_ties.append(('.' + master, '.' + slave))
                    instance = Instance(
                        args[0] == 'prim', instance_type, instance_name,
                        coord, rotation, *args[6:]
                    )
                    for pin, net in instance.get_pinmap():
                        if pin.is_output():
                            mode = 'driver' if args[0] == 'prim' else 'in'
                        else:
                            mode = 'user' if args[0] == 'prim' else 'out'
                        self._net_insns.append((
                            net,
                            pin.get_layer(),
                            pin.get_coord(),
                            transrot(pin.get_translate(), coord, rotation),
                            pin.get_rotate() + rotation,
                            mode
                        ))
                    self._instances.append(instance)
                    for direction, vhd_type, iface in instance.get_data().get_interfaces():
                        self._interfaces.append((direction, vhd_type, '{}_{}'.format(instance_name, iface)))
                    continue

                if args[0] == 'route':
                    self._routers.append((cols.convert(args[1]), ['.{}'.format(x) for x in args[2:]]))
                    continue

                if args[0] == 'shunt':
                    coord1 = (cols.convert(args[1]), rows.convert(args[2]))
                    net1 = '.{}'.format(args[3])
                    coord2 = (cols.convert(args[4]), rows.convert(args[5]))
                    net2 = '.{}'.format(args[6])
                    layer = args[7]
                    self._shunters.append((coord1, coord2, layer))
                    self._net_insns.append((net1, layer, (0, 0), coord1, 0, 'user'))
                    self._net_insns.append((net2, layer, (0, 0), coord2, 0, 'user'))
                    continue

                if args[0] in 'outline':
                    self._outlines.append((
                        cols.convert(args[1]),
                        rows.convert(args[2]),
                        cols.convert(args[3]),
                        rows.convert(args[4]),
                        from_mm(args[5]), # expansion
                        from_mm(args[6]), # corner radius
                        args[7]
                    ))
                    continue

                if args[0] == 'text':
                    text = args[1].replace('~', ' ')
                    coord = (cols.convert(args[3]) + from_mm(args[5]), rows.convert(args[4]) + from_mm(args[6]))
                    rotation = float(args[2]) / 180 * math.pi
                    scale = float(args[7]) if len(args) > 7 else 1.0
                    halign = float(args[8]) if len(args) > 8 else 0.5
                    valign = float(args[9]) if len(args) > 9 else None
                    self._labels.append(Label(text, coord, rotation, scale, halign, valign))
                    continue

                print('warning: unknown subcircuit construct: {}'.format(line))

        forwarded_pin_nets = set()
        for direction, name in forwarded_pins:
            insns = []
            for insn in self._net_insns:
                if insn[0] == '.' + name:
                    insns.append(insn)
            if not insns:
                raise ValueError('cannot forward pins for nonexistant net {}'.format(name))
            if len(insns) != 1:
                raise ValueError('multiple pins exist for forwarded pin {}; this is not supported'.format(name))
            net, layer, coord, translate, rotate, mode = insns[0]
            self._pins.add(name, direction, layer, coord, translate, rotate)
            self._net_insns.append((net, layer, coord, translate, rotate, direction))
            forwarded_pin_nets.add(net)

        print('doing basic DRC for subcircuit {}...'.format(self._name))
        netlist = Netlist()
        for net, layer, coord, translate, rotate, mode in self._net_insns:
            netlist.add(net, layer, transrot(coord, translate, rotate), mode)
        good = netlist.check_subcircuit()
        unrouted = set(map(lambda x: x.get_name(), netlist.iter_logical()))
        routed = set()
        for x, nets in self._routers:
            ranges = []
            for net in nets:
                if net in routed:
                    print('net {} is routed multiple times'.format(net))
                    good = False
                elif net not in unrouted:
                    print('net {} cannot be routed because it does not exist'.format(net))
                    good = False
                routed.add(net)
                unrouted.remove(net)
                y_min = None
                y_max = None
                for _, (_, y), _ in netlist.get_logical(net).iter_points():
                    if y_min is None or y < y_min:
                        y_min = y
                    if y_max is None or y > y_max:
                        y_max = y
                assert y_min is not None
                assert y_max is not None
                ranges.append((y_min, y_max, net))
            ranges.sort()
            for i in range(len(ranges) - 1):
                if ranges[i+1][0] - from_mm(0.5) < ranges[i][1]:
                    print('nets {} and {} overlap in routing column;'.format(ranges[i][2], ranges[i+1][2]))
                    print('  {} goes down to Y{} and {} up to Y{} at X{}'.format(
                        ranges[i+1][2], to_mm(ranges[i+1][0]),
                        ranges[i][2], to_mm(ranges[i][1]),
                        to_mm(x)))
                    good = False
        for net in forwarded_pin_nets:
            if net in routed:
                print('net {} is routed multiple times'.format(net))
                good = False
            elif net not in unrouted:
                print('net {} cannot be routed because it does not exist'.format(net))
                good = False
            routed.add(net)
            unrouted.remove(net)
        for net in unrouted:
            print('net {} is not routed'.format(net))
            good = False
        if not good:
            raise ValueError('basic DRC failed for subcircuit {}'.format(self._name))
        print('finished loading subcircuit {}, basic DRC passed'.format(self._name))

        # Write VHDL for the netlist.
        with open(os.path.join('subcircuits', self._name, '{}.gen.vhd'.format(self._name)), 'w') as f:
            f.write('library ieee;\nuse ieee.std_logic_1164.all;\n\nentity {} is\n  port (\n'.format(self._name))

            pin_directions = {}
            for pin in self._pins:
                direction = pin.get_direction()
                pin = pin.get_name().split('~')[0].split('*')[0]
                if pin in pin_directions:
                    if direction == 'out':
                        pin_directions[pin] = 'out'
                else:
                    pin_directions[pin] = direction

            first = True
            nets = set()
            for pin in self._pins:
                pin = pin.get_name().split('~')[0].split('*')[0]
                direction = pin_directions[pin]
                if pin in nets:
                    continue
                nets.add(pin)
                if first:
                    first = False
                else:
                    f.write(';\n')
                f.write('    {} : {} std_logic'.format(pin, direction))
            for direction, vhd_type, iface_name in self._interfaces:
                if first:
                    first = False
                else:
                    f.write(';\n')
                f.write('    if_{} : {} {}'.format(iface_name, direction, vhd_type))
            f.write('\n  );\nend entity;\n\narchitecture model of {} is\n'.format(self._name))
            for net, *_ in self._net_insns:
                if not net.startswith('.'):
                    continue
                net = net[1:].split('~')[0].split('*')[0]
                if net in nets:
                    continue
                f.write('  signal {} : std_logic;\n'.format(net))
                nets.add(net)
            f.write('begin\n\n')
            for instance in self._instances:
                name = instance.get_name().replace('*', '').replace('~', '')
                f.write('  {}_inst: entity work.{}\n    port map (\n'.format(name, instance.get_data().get_name()))
                pins = set()
                first = True
                for pin, net in instance.get_pinmap():
                    pin = pin.get_name().split('~')[0].split('*')[0]
                    if pin in pins:
                        continue
                    pins.add(pin)
                    net = net[1:].split('~')[0].split('*')[0]
                    if first:
                        first = False
                    else:
                        f.write(',\n')
                    f.write('      {} => {}'.format(pin, net))
                for _, _, iface_name in instance.get_data().get_interfaces():
                    if first:
                        first = False
                    else:
                        f.write(',\n')
                    f.write('      if_{} => if_{}_{}'.format(iface_name, name, iface_name))
                f.write('\n    );\n\n')
            f.write('end architecture;\n')
Exemple #11
0
 def convert(self, pos):
     """Converts a grid position to internal units."""
     pos = float(pos)
     if pos < 0:
         pos += len(self._positions)
     return from_mm(self._convert_mm(pos))
Exemple #12
0
 def get_bridge_r(y):
     sx, sy = get_scale((self._x, y))
     rx = int(round(sx * from_mm(0.4))) # <-- desired global X radius
     ry = int(round(sy * from_mm(0.6))) # <-- desired global Y radius
     return rx, ry
Exemple #13
0
 def trace(path, layer, t=0.2):
     path = transformer.path_to_global(path, translate, rotate, True)
     pcb.add_trace(layer, from_mm(t), *path)
Exemple #14
0
    def generate(self, pcb, transformer, translate, rotate):
        if len(self._targets) < 2:
            return

        if config.LAYOUT_ONLY:
            min_y = self._targets[0][0][1]
            max_y = min_y
            for coord, _ in self._targets:
                path = [
                    (coord[0], coord[1]),
                    (self._x, coord[1])
                ]
                path = transformer.path_to_global(path, translate, rotate, True)
                pcb.add_trace('GTO', from_mm(0.2), *path)
                min_y = min(min_y, coord[1])
                max_y = max(max_y, coord[1])
            path = [
                (self._x, min_y),
                (self._x, max_y)
            ]
            path = transformer.path_to_global(path, translate, rotate, True)
            pcb.add_trace('GTO', from_mm(0.2), *path)
            return

        def glob(coord):
            return transformer.to_global(coord, translate, rotate, True)

        def get_scale(coord, r=from_mm(1)):
            a = glob((coord[0] + r, coord[1]))
            b = glob((coord[0] - r, coord[1]))
            x = 2*r / math.hypot(a[0] - b[0], a[1] - b[1])
            a = glob((coord[0], coord[1] + r))
            b = glob((coord[0], coord[1] - r))
            y = 2*r / math.hypot(a[0] - b[0], a[1] - b[1])
            return (x, y)

        def trace(path, layer, t=0.2):
            path = transformer.path_to_global(path, translate, rotate, True)
            pcb.add_trace(layer, from_mm(t), *path)

        # First, turn the bridge points into ranges: both the arc for the
        # visual representation and potential vias/knots right next to it cost
        # space. We can move the vias/knots slightly, but we can't move the
        # bridges, because the horizontal traces they cross are generated by
        # other routing columns. Note that we might have bridges registered
        # with us that lie beyond the actual column; we ignore those here.
        # The final result is an ordered list of four-tuples representing
        # non-overlapping ranges of local Y coordinates (first and second
        # element being the lower and upper coordinate) of at least 2ry in
        # length, where rx/ry is the local radius of the graphic needed to
        # get the desired radius in global coordinates, as stored in the third
        # and fourth tuple element.
        def get_bridge_r(y):
            sx, sy = get_scale((self._x, y))
            rx = int(round(sx * from_mm(0.4))) # <-- desired global X radius
            ry = int(round(sy * from_mm(0.6))) # <-- desired global Y radius
            return rx, ry
        all_y_targets = [y for (_, y), _ in self._targets]
        min_y = min(all_y_targets)
        max_y = max(all_y_targets)
        bridge_reqs = []
        for y in self._bridges:
            if y < min_y or y > max_y:
                continue
            rx, ry = get_bridge_r(y)
            bridge_reqs.append((y - ry, y + ry, rx, ry))
        bridge_reqs.sort()
        combined_bridges = []
        for a, b, rx, ry in bridge_reqs:
            if not combined_bridges or combined_bridges[-1][1] < a:
                combined_bridges.append((a, b, rx, ry))
            else:
                combined_bridges[-1] = (combined_bridges[-1][0], b, None, None)
        bridges = []
        for a, b, rx, ry in combined_bridges:
            if rx is None or ry is None:
                rx, ry = get_bridge_r((a + b) / 2)
            if b - a < 2 * ry:
                ry = int(b - a) // 2
            bridges.append((a, b, rx, ry))

        for i in range(len(bridges)-1):
            assert bridges[i+1][0] >= bridges[i][1]

        # Let's call the bits before, between, and after the bridges spans
        # (note that by definition there is at least one of these). Two Y
        # coordinate ranges are involved in a span; the outer range specifies
        # which connection points are mapped to which span, while the inner
        # range specifies the outermost coordinates that a knot may exist at.
        # Let's set up those ranges first.
        spans = []
        min_y -= from_mm(10)
        max_y += from_mm(10)
        for i in range(len(bridges) + 1):
            spans.append((
                min_y if i == 0            else (bridges[i-1][0] + bridges[i-1][1]) / 2,
                max_y if i == len(bridges) else (bridges[i  ][0] + bridges[i  ][1]) / 2,
                min_y if i == 0            else bridges[i-1][1],
                max_y if i == len(bridges) else bridges[i  ][0],
                []
            ))

        # Spans consist of zero or more knots. A knot is a point in the routing
        # column where the column meets with one or more connection points;
        # depending on the amount, and where the knot is in the column, the
        # point will be rendered as a bend or as a thick dot, and may or may
        # not receive a via at its centerpoint. While it's possible that there
        # are two connection points with the same Y coordinate on either side
        # of the column, multiple connection points on the same side may also
        # be routed to the same knot if they are too close. In this case the
        # final stretch of the horizontal trace will bend toward the
        # centerpoint of the knot.
        class Knot:
            def __init__(self, x, y):
                super().__init__()
                self._x = x
                self._y = y
                self._r = None
                self._targets = [] # (x, y), layer
                self._min_y = y
                self._max_y = y
                self._type = 'knot' # or 'upper', 'lower', or 'single' for endpoints
                self.prev_layer = None
                self.next_layer = None

            def get_coord(self):
                return (self._x, self._y)

            def get_y(self):
                return self._y

            def set_y(self, y):
                self._y = y
                self._r = None

            def recenter_y(self):
                self.set_y(int(round((self._min_y + self._max_y) / 2)))

            def add_target(self, coord, layer):
                self._targets.append((coord, layer))
                self._min_y = min(self._min_y, coord[1])
                self._max_y = max(self._max_y, coord[1])

            def iter_targets(self):
                for target in self._targets:
                    yield target

            def get_r(self):
                if self._r is None:
                    sx, sy = get_scale((self._x, self._y))
                    rx = int(round(sx * from_mm(0.5))) # <-- desired global X radius
                    ry = int(round(sy * from_mm(0.5))) # <-- desired global Y radius
                    self._r = (rx, ry)
                return self._r

            def mark_constrained(self):
                self._constrained = True

            def mark_endpoint(self, typ, limit=None):
                if len(self._targets) > (2 if typ == 'single' else 1):
                    return
                self._type = typ
                if typ == 'upper':
                    self.set_y(max(self._y - self.get_r()[1], limit))
                elif typ == 'lower':
                    self.set_y(min(self._y + self.get_r()[1], limit))
                else:
                    self.recenter_y()

            def get_layers(self):
                layers = set()
                for _, layer in self._targets:
                    layers.add(layer)
                if self.prev_layer is not None:
                    layers.add(self.prev_layer)
                if self.next_layer is not None:
                    layers.add(self.next_layer)
                return layers

            def layer_preference(self):
                layers = self.get_layers()
                if len(layers) == 1:
                    return next(iter(layers))
                return None

            def needs_via(self, primary_layer):
                return len(self.get_layers()) > 1

            def needs_dot(self):
                return int(self.prev_layer is not None) + int(self.next_layer is not None) + len(self._targets) > 2

            def bend_90(self):
                return self._type in ('upper', 'lower')

        # Now lets assign connection points to the spans. Initially, we'll just
        # give each point its own knot. We'll also immediately place vias for
        # points that don't connect on the primary layer.
        for (tx, ty), layer in sorted(self._targets, key=lambda x: x[0][1]):
            for a, b, _, _, knots in spans:
                if ty >= a and ty < b:
                    k = Knot(self._x, ty)

                    # If the point is not on the primary layer and it's close
                    # enough to our column, we'll actually route the horizontal
                    # on that layer, to prevent vias from getting too close.
                    # But otherwise, place a via at the connection point now
                    # and route on primary.
                    if layer != self._pl:
                        a = glob((tx, ty))
                        b = glob((self._x, ty))
                        if math.hypot(a[0] - b[0], a[1] - b[1]) > from_mm(0.5):
                            layer = self._pl
                            pcb.add_via(a)

                    k.add_target((tx, ty), layer)
                    knots.append(k)
                    break
            else:
                # This should never happen; if it does, min_y and max_y are not
                # correct or the outer spans do not properly extend to those
                # limits.
                assert False

        # The first and last span should each have at least one knot.
        while not spans[0][4]:
            del spans[0]
        while not spans[-1][4]:
            del spans[-1]
        if not spans[0][4] or not spans[-1][4]:
            print('spans for net {}:'.format(self._net))
            for oa, ob, ia, ib, knots in spans:
                print(' - span from {} to {} ({} to {}) has {} knots'.format(
                    to_mm(oa), to_mm(ob), to_mm(ia), to_mm(ib), len(knots)))
            assert False

        # Now combine knots that are too close together, starting with the
        # closest ones.
        for _, _, _, _, knots in spans:
            done = False
            while not done:
                i = -1
                done = True
                while i < len(knots) - 2:
                    i += 1
                    k1 = knots[i]
                    k2 = knots[i+1]
                    d12 = k2.get_y() - k1.get_y()
                    assert d12 >= 0
                    if i+2 < len(knots):
                        k3 = knots[i+2]
                        d23 = k3.get_y() - k2.get_y()
                        assert d23 >= 0
                        if d23 < d12:

                            # The next two knots are closer together than
                            # these two; try combining those first.
                            continue

                    min_d = k1.get_r()[1] + k2.get_r()[1]
                    if d12 < min_d:

                        # Knots k1 and k2 are too close, combine.
                        del knots[i+1]
                        for target in k2.iter_targets():
                            k1.add_target(*target)
                        k1.recenter_y()
                        done = False

        # The knots at the start and end of the span may interfere with
        # bridges. Combine any knots that interfere this way for the
        # lower end of the column...
        for _, _, a, _, knots in spans:
            ka = Knot(self._x, a)
            endpt_min_y = a + ka.get_r()[1]
            ka.set_y(endpt_min_y)
            ka.mark_constrained()
            #a = ka.get_y()
            any_merged = False
            while knots:
                k = knots[0]
                if k.get_y() - k.get_r()[1] < a:
                    del knots[0]
                    any_merged = True
                    for target in k.iter_targets():
                        ka.add_target(*target)
                    a = ka.get_y() + ka.get_r()[1] * 2
                else:
                    break
            if any_merged:
                knots.insert(0, ka)

        # ...and for the upper end.
        for _, _, _, b, knots in reversed(spans):
            kb = Knot(self._x, b)
            endpt_max_y = b - kb.get_r()[1]
            kb.set_y(endpt_max_y)
            kb.mark_constrained()
            #b = kb.get_y()
            any_merged = False
            while knots:
                k = knots[-1]
                if k.get_y() + k.get_r()[1] > b:
                    del knots[-1]
                    any_merged = True
                    for target in k.iter_targets():
                        kb.add_target(*target)
                    b = kb.get_y() - kb.get_r()[1] * 2
                else:
                    break
            if any_merged:
                knots.append(kb)

        # We don't need the span ranges anymore now, so we can simplify the
        # data structure for them.
        spans = [knots for _, _, _, _, knots in spans]

        # Mark the last knots at each end as endpoints. This may move their
        # "center" inwards to make way for a bend, but no further than the
        # constraint for the next bridge.
        if len(spans) == 1 and len(spans[0]) == 1:
            # Special case for just one knot.
            spans[0][0].mark_endpoint('single')
        else:
            # Multiple knots, mark endpoints.
            spans[0][0].mark_endpoint('lower', endpt_max_y)
            spans[-1][-1].mark_endpoint('upper', endpt_min_y)

        # Now we have our topology mostly worked out. The only thing that
        # remains is to figure out what layers to place the vertical components
        # of the traces on. We'll actually draw the traces as we do so.
        kp = None
        ks = None
        force_nonprimary = False
        for knots in spans:
            for k in knots:
                if kp is not None:

                    # We have two knots that need to be connected on a layer.
                    # Figure out what the best layer would be.
                    preference = kp.layer_preference()
                    if preference is not None:
                        layer = preference
                    else:
                        preference = k.layer_preference()
                        if preference is not None:
                            layer = preference
                        else:
                            layer = self._pl
                    if force_nonprimary and layer == self._pl:
                        layer = self._sl

                    # Store the layers in the knots.
                    kp.next_layer = layer
                    k.prev_layer = layer

                    # Don't render the trace immediately; optimize by drawing
                    # contiguous lines with one path.
                    if ks is not None and ks.next_layer != layer:

                        # Draw from ks to k.
                        trace([ks.get_coord(), kp.get_coord()], ks.next_layer)
                        ks = kp

                if ks is None:
                    ks = k

                kp = k

                # Next knot in this span (if any remain) will not cross
                # bridges, and thus can be routed on the primary layer.
                force_nonprimary = False

            # Next knot (if any) will cross bridges. So we need to route
            # on secondary.
            force_nonprimary = True

        if ks is not None and ks is not kp:
            trace([ks.get_coord(), kp.get_coord()], ks.next_layer)

        # Place vias for the knots that need one.
        for knots in spans:
            for knot in knots:
                if knot.needs_via(self._pl):
                    pcb.add_via(glob(knot.get_coord()))

        # Render the connections from the column to the connection points.
        for knots in spans:
            for knot in knots:
                kx, ky = knot.get_coord()
                rx, ry = knot.get_r()
                for (tx, ty), layer in knot.iter_targets():
                    if ty == ky and tx == kx:

                        # No trace necessary, we're already there.
                        continue

                    elif ty == ky or tx == kx:

                        # No curvature necessary.
                        path = [(kx, ky), (tx, ty)]

                    else:

                        bend_90 = knot.bend_90()

                        # X coordinate for the bend.
                        if bend_90:
                            bx = min(max(kx - rx, tx), kx + rx)
                        else:
                            bx = min(max(kx - rx * 1.5, tx), kx + rx * 1.5)

                        path = []
                        N = 3
                        for i in range(N+1):
                            f = i / N
                            if bend_90:
                                a = f * math.pi / 2
                                c = 1 - math.cos(a)
                                s = math.sin(a)
                            else:
                                a = (1 + f) * math.pi / 4
                                c = 1 - math.cos(a) * math.sqrt(2)
                                s = (math.sin(a) - 1/math.sqrt(2)) * 3.4142135623730945
                            x = kx + (bx - kx) * c
                            y = ky + (ty - ky) * s
                            path.append((int(x), int(y)))
                        path.append((tx, ty))

                    path = transformer.path_to_global(path, translate, rotate, True)
                    pcb.add_trace(layer, from_mm(0.2), *path)
                    pcb.add_trace('GTO', from_mm(0.2), *path)

        # Place dots on the knots that need one.
        for knots in spans:
            for knot in knots:
                if knot.needs_dot():
                    trace([knot.get_coord()], 'GTO', 0.8)

        # Finally, draw the top overlay path for the vertical column.
        path = [spans[0][0].get_coord()]
        if spans[-1][-1].get_coord() != path[0]:
            N = 3
            for a, b, rx, ry in bridges:
                for i in range(N+1):
                    f = i / N
                    th = f * math.pi / 2
                    c = 1 - math.cos(th)
                    s = math.sin(th)
                    x = self._x + s * rx
                    y = a + c * ry
                    path.append((int(x), int(y)))
                for i in reversed(range(N+1)):
                    f = i / N
                    th = f * math.pi / 2
                    c = 1 - math.cos(th)
                    s = math.sin(th)
                    x = self._x + s * rx
                    y = b - c * ry
                    path.append((int(x), int(y)))
            path.append(spans[-1][-1].get_coord())
            trace(path, 'GTO')