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 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)
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)
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)
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)
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))
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')
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))
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
def trace(path, layer, t=0.2): path = transformer.path_to_global(path, translate, rotate, True) pcb.add_trace(layer, from_mm(t), *path)
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')