Source code for gdsCAD.shapes

# -*- coding: utf-8 -*-
"""
Classes to define simple shapes.

Filled Objects
--------------
:class:`Rectangle`
    A filled rectangle
:class:`Disk`
    A filled circle
:class:`Ellipse`
    A filled ellipse
:class:`RegPolygon`
    A filled regular polygon
:class:`Label`
    Printing text

Unfilled Objects
----------------
:class:`Box`
    An unfilled rectangle
:class:`RegPolyline`
    An ufilled regular polygon
:class:`Circle`
    An unfilled circle


.. note::
    Copyright 2009-2012 Lucas Heitzmann Gabrielli
    
    Copyright 2013 Andrew G. Mark

    gdsCAD (based on gdspy) is released under the terms of the GNU GPL
    
"""

import os
import numpy as np

import core


[docs]class Rectangle(core.Boundary): """ Filled rectangular geometric object. :param point1: Coordinates of a corner of the rectangle. :param point2: Coordinates of the corner of the rectangle opposite to ``point1``. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Examples:: rectangle = shapes.Rectangle((0, 0), (10, 20)) myCell.add(rectangle) """ def __init__(self, point1, point2, layer=None, datatype=None): points = np.array([[point1[0], point1[1]], [point1[0], point2[1]], [point2[0], point2[1]], [point2[0], point1[1]]]) core.Boundary.__init__(self, points, layer, datatype) def __str__(self): return "Rectangle (({0[0]}, {0[1]}) to ({1[0]}, {1[1]}), layer {2}, datatype {3})".format(self.points[0], self.points[2], self.layer, self.datatype) def __repr__(self): return "Rectangle({2}, ({0[0]}, {0[1]}), ({1[0]}, {1[1]}), {3})".format(self.points[0], self.points[2], self.layer, self.datatype)
[docs]class Box(core.Path): """ Unfilled rectangular geometric object. :param point1: Coordinates of a corner of the rectangle. :param point2: Coordinates of the corner of the rectangle opposite to ``point1``. :param width: The width of the line :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Examples:: box = shapes.Box((0, 0), (10, 20), 0.5) myCell.add(box) """ def __init__(self, point1, point2, width, layer=None, datatype=None): points = np.array([[point1[0], point1[1]], [point1[0], point2[1]], [point2[0], point2[1]], [point2[0], point1[1]], [point1[0], point1[1]]]) core.Path.__init__(self, points, width, layer, datatype) def __str__(self): return "Box (({0[0]}, {0[1]}) to ({1[0]}, {1[1]}), layer {2}, datatype {3})".format(self.points[0], self.points[2], self.layer, self.datatype) def __repr__(self): return "Box ({2}, ({0[0]}, {0[1]}), ({1[0]}, {1[1]}), {3})".format(self.points[0], self.points[2], self.layer, self.datatype)
[docs]class Disk(core.Boundary): """ A filled circle, or section of a circle :param center: Coordinates of the disk's center. :param radius: The radius of the disk :param inner_radius: The inner radius of the disk. If absent creates a solid disk. :param initial_angle: The starting angle of the sweep :param final_angle: The final angle of the sweep :param number_of_points: The number of line segments that the disk will be composed of :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Example:: disk=shapes.Disk((-5,-5), 5) disk.show() """ def __init__(self, center, radius, inner_radius=0, initial_angle=0, final_angle=0, number_of_points=199, layer=None, datatype=None): self.center = center self.radius = radius if final_angle == initial_angle: final_angle += 360.0 angles = np.linspace(initial_angle, final_angle, number_of_points).T * np.pi/180. points=np.vstack((np.cos(angles), np.sin(angles))).T * radius + np.array(center) if inner_radius != 0: points2 = np.vstack((np.cos(angles), np.sin(angles))).T * inner_radius + np.array(center) points=np.vstack((points, points2[::-1])) core.Boundary.__init__(self, points, layer, datatype) def __str__(self): return "Disk Boundary (center={}, radius={}, layer={}, datatype={})".format(self.center, self.radius, self.layer, self.datatype)
[docs]class Circle(core.Path): """ An unfilled circular path or section or arc. :param center: Coordinates of the disk's center. :param radius: The radius of the disk. :param width: The width of the line. :param initial_angle: The starting angle of the sweep. :param final_angle: The final angle of the sweep. :param number_of_points: The number of line segments that the disk will be composed of. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Example:: circ=shapes.Circle((10,10), 10, 0.5) circ.show() """ def __init__(self, center, radius, width, initial_angle=0, final_angle=0, number_of_points=199, layer=None, datatype=None): self.center = center self.radius = radius if final_angle == initial_angle: final_angle += 360.0 angles = np.linspace(initial_angle, final_angle, number_of_points) * np.pi/180. points=np.vstack((np.cos(angles), np.sin(angles))).T * radius + np.array(center) core.Path.__init__(self, points, width, layer, datatype) def __str__(self): return "Circle Path ({} points, width {}, layer {}, datatype {})".format(len(self.points), self.width, self.layer, self.datatype)
[docs]class Ellipse(core.Boundary): """ A filled ellipse, or section of an ellipse :param center: Coordinates of the ellipse's center. :param radius_x: The radius of the ellipse along x :param radius_y: The radius of the ellipse along y :param inner_radius_x: The inner radius of the ellipse along x. If absent creates a solid ellipse. :param inner_radius_y: The inner radius of the ellipse along y. If absent creates a solid ellipse. :param initial_angle: The starting angle of the sweep :param final_angle: The final angle of the sweep :param number_of_points: The number of line segments that the ellipse will be composed of :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Example:: ellipse=shapes.Ellipse((-5,-5), 2, 4) ellipse.show() """ def __init__(self, center, radius_x, radius_y, inner_radius_x=0, inner_radius_y=0, initial_angle=0, final_angle=0, number_of_points=199, layer=None, datatype=None): self.center = center self.radius_x = radius_x self.radius_y = radius_y if final_angle == initial_angle: final_angle += 360.0 angles = np.linspace(initial_angle, final_angle, number_of_points).T * np.pi/180. points=np.vstack((radius_x*np.cos(angles), radius_y*np.sin(angles))).T + np.array(center) if inner_radius_x != 0 and inner_radius_y != 0: points2 = np.vstack((inner_radius_x*np.cos(angles), inner_radius_y*np.sin(angles))).T + np.array(center) points=np.vstack((points, points2[::-1])) core.Boundary.__init__(self, points, layer, datatype) def __str__(self): return "Ellipse Boundary (center={}, radius_x={}, radius_y={}, layer={}, datatype={})".format(self.center, self.radius_x, self.radius_y, self.layer, self.datatype)
[docs]class RegPolygon(core.Boundary): """ An unfilled regular polgyon. :param center: Coordinates of the disk's center. :param length: The length of an edge. :param N: The number of sides :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Example:: pent = shapes.RegPolygon((10,10), 10, 5) pent.show() """ def __init__(self, center, length, N, layer=None, datatype=None): self.center = center self.length = length self.N = N angles = np.linspace(0, 360, N, endpoint=False) * np.pi/180. alpha = angles[1] radius = length / np.sin(alpha/2) /2. points=np.vstack((np.cos(angles), np.sin(angles))).T * radius + np.array(center) core.Boundary.__init__(self, points, layer, datatype) def __str__(self): return "RegPolygon Boundary ({} points, width {}, layer {}, datatype {})".format(len(self.points), self.width, self.layer, self.datatype)
[docs]class RegPolyline(core.Path): """ An unfilled regular polgyon. :param center: Coordinates of the disk's center. :param length: The length of an edge. :param N: The number of sides :param width: The width of the line. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Example:: hex=shapes.RegPolylone((10,10), 10, 6, 0.5) hex.show() """ def __init__(self, center, length, N, width, layer=None, datatype=None): self.center = center self.length = length self.N = N angles = np.linspace(0, 360, N+1, endpoint=True) * np.pi/180. alpha = angles[1] radius = length / np.sin(alpha/2) /2. points=np.vstack((np.cos(angles), np.sin(angles))).T * radius + np.array(center) core.Path.__init__(self, points, width, layer, datatype) def __str__(self): return "RegPolyine Path ({} points, width {}, layer {}, datatype {})".format(len(self.points), self.width, self.layer, self.datatype)
[docs]class Label(core.Elements): """ Printing text string object. Each letter is formed by a series of polygons collected together as an Elements list. :param text: The text to be converted in geometric objects. :param size: Base size of each character. :param position: Text position (lower left corner). :param horizontal: If ``True``, the text is written from left to right; if ``False``, from top to bottom. :param angle: The angle of rotation of the text. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Examples:: text = shapes.Label('Sample text', 20, (-10, -100)) text.show() myCell.add(text) """ from font import _font def __init__(self, text, size, position=(0, 0), horizontal=True, angle=0, layer=None, datatype=None) : self.text=text self.position=position polygons = [] posX = 0 posY = 0 text_multiplier = size / 9.0 for jj in range(len(text)): if text[jj] == '\n': if horizontal: posY -= 11 posX = 0 else: posX += 8 posY = 0 elif text[jj] == '\t': if horizontal: posX = posX + 32 - (posX + 8) % 32 else: posY = posY - 11 - (posY - 22) % 44 else: if Label._font.has_key(text[jj]): for p in Label._font[text[jj]]: polygon = p[:] for ii in range(len(polygon)): xp = text_multiplier * (posX + polygon[ii][0]) yp = text_multiplier * (posY + polygon[ii][1]) polygon[ii] = (xp, yp) polygons.append(np.array(polygon)) if horizontal: posX += 8 else: posY -= 11 core.Elements.__init__(self, polygons, layer, datatype) self.rotate(angle) self.translate(position) def __str__(self): return "Text -\"{}\" layer={}".format(self.text, self.layer)
[docs]class LineLabel(core.Elements): """ Printing text string object as Line font. Each letter is formed by a series of lines collected together as an Elements list. The lines are Hershey vector fonts. The font is basically a collection of symbols, containing all kinds of font styles and symbols. The font itself is not a monotype and no guarantee is given on the height of the characters. But for normal characters, the height should always be within the specified size. Since the Hershey nearly as old as ASCII, it is unfortunately not ordered in these codes. The font itself also contains many styles, ranging from Cyrillic over Greek to Roman letters. For convenience, lookup tables from ASCII to symbol number are provided for several font styles. But note, that only a fraction of all available symbols are mapped to ASCII characters in these tables. You can always add a specific symbol by yourself, once you know its Hershey code. +-------------+--------------------------+ | Table name | Description | +=============+==========================+ | gothgbt | Gothic English Triplex | +-------------+--------------------------+ | gothgrt | Gothic German Triplex | +-------------+--------------------------+ | gothitt | Gothic Italian Triplex | +-------------+--------------------------+ | greekc | Greek Complex | +-------------+--------------------------+ | greekcs | Greek Complex Small | +-------------+--------------------------+ | greekp | Greek Plain | +-------------+--------------------------+ | greeks | Greek Simplex | +-------------+--------------------------+ | cyrilc | Cyrillic complex | +-------------+--------------------------+ | italicc | Italic Complex | +-------------+--------------------------+ | italiccs | Italic Complex Small | +-------------+--------------------------+ | italict | Italic Triplex | +-------------+--------------------------+ | scriptc | Script Complex | +-------------+--------------------------+ | scripts | Script Simplex | +-------------+--------------------------+ | romanc | Roman Complex | +-------------+--------------------------+ | romancs | Roman Complex Small | +-------------+--------------------------+ | romand | Roman Duplex | +-------------+--------------------------+ | romanp | Roman Plain | +-------------+--------------------------+ | romans | Roman Simplex | +-------------+--------------------------+ | romant | Roman Triplex | +-------------+--------------------------+ :param text: The text to be converted in geometric objects. :param size: Base size of each character. :param position: Text position (lower left corner). :param style: The default name of ASCII lookup table. Defaults to (romans) Roman Simplex. :param horizontal: If ``True``, the text is written from left to right; if ``False``, from top to bottom. :param line_width: Line width of the text. Defaults to one 40th of the text size. :param layer: The GDSII layer number for this element. Defaults to layer of 1st object, or core.default_layer. :param datatype: The GDSII datatype for this element (between 0 and 255). Examples:: text = shapes.LineLabel(20, (-10, -100)) text.add_text('Sample text', 'romand') text.show() myCell.add(text) """ _DEFAULT_CHAR_HEIGHT = 40. _hershey_table = dict() _hershey_ascii_lookup_table = dict() def __init__(self, text, size, position=(0, 0), style='romans', horizontal=True, line_width=None, layer=None, datatype=None): if not len(self._hershey_table): self._load_hersey_table() self._scale = size / self._DEFAULT_CHAR_HEIGHT self._line_width = line_width if line_width else size/40. self._layer = layer self._datatype = datatype self._origin = position self._style = style self._horizontal = horizontal self._symbols = list() self._symbol_pos = list(position) core.Elements.__init__(self) self.add_text(text, style) def _load_hersey_table(self): """ Load the hersey table. This modifies a static class variable thus avoiding to read the Hersey table multiple times. """ self._hershey_table.clear() path, _ = os.path.split(__file__) fname = os.path.join(path, 'resources', 'hershey', 'hershey') # Read lines and correct newlines lines = [] append_next_line = False vertices_count = 0 target_vertices_count = 0 for line in open(fname, 'r').readlines(): line = line.rstrip('\n') # Skip empty lines if not len(line): continue if vertices_count == target_vertices_count: # New entry begins here target_vertices_count = int(line[5:8]) * 2 vertices_count = len(line[8:]) lines.append(line) else: assert vertices_count < target_vertices_count, 'Got more vertices then specified' vertices_count += len(line) lines[-1] += line # Parse the lines we have just read for line in lines: # Skip empty lines if not len(line): continue char_id = int(line[:5]) n_vertices = int(line[5:8]) left, right = [ord(c) - ord('R') for c in line[8:10]] vertices = line[10:] assert len(vertices) % 2 == 0, 'Number coordinates needs to be even since it comes in pairs' # Convert ascii coordinates to true paths strokes = list() current_stroke = list() for coordinate in zip(vertices[::2], vertices[1::2]): if coordinate[0] == ' ' and coordinate[1] == 'R': strokes.append(np.array(current_stroke, dtype=np.int)) current_stroke = list() continue tmp_coords = [ord(coordinate[i]) - ord('R') for i in (0, 1)] tmp_coords[1] = -tmp_coords[1] current_stroke.append(tmp_coords) strokes.append(np.array(current_stroke, dtype=np.int)) self._hershey_table[char_id] = {'strokes': strokes, 'left_pos': left, 'right_pos': right} def _load_hersey_ascii_lookup_table(self, table_name): """ Load an ASCII lookup table for the given font name. This modifies a static class variable thus avoiding to read the lookup table multiple times. :param table_name: The table name of the lookup table. """ path, _ = os.path.split(__file__) fname = os.path.join(path, 'resources', 'hershey', '%s.hmp' % table_name) to_ascii = dict() from_ascii = dict() current_code = 32 for line in open(fname, 'r').readlines(): for definition in line.rstrip().split(): # Either one number or a range of number is specified if not '-' in definition: to_ascii[int(definition)] = current_code from_ascii[current_code] = int(definition) current_code += 1 else: start, stop = [int(x) for x in definition.split('-')] for i in range(start, stop+1): to_ascii[i] = current_code from_ascii[current_code] = i current_code += 1 self._hershey_ascii_lookup_table[table_name] = {'to_ascii': to_ascii, 'from_ascii': from_ascii} def _add_single_symbol(self, symbol): """ Internal function which adds one single symbol :param symbol: Symbol code """ # Keep track of symbols to know what we have painted here self._symbols.append(symbol) assert symbol in self._hershey_table, 'This symbol is not in the Hershey table' symbol = self._hershey_table[symbol] for stroke in symbol['strokes']: if not len(stroke): continue scaled_stroke = (stroke + [0, self._DEFAULT_CHAR_HEIGHT/2]) * self._scale stroke_path = core.Path(scaled_stroke, self._line_width, self.layer, pathtype=2, datatype=self.datatype) stroke_path.translate([self._symbol_pos[0] - symbol['left_pos'] * self._scale, self._symbol_pos[1]]) self.add(stroke_path) if self._horizontal: self._symbol_pos[0] += (symbol['right_pos'] - symbol['left_pos']) * self._scale else: self._symbol_pos[0] = self._origin[0] self._symbol_pos[1] -= self._DEFAULT_CHAR_HEIGHT * self._scale
[docs] def add_symbol(self, symbol): """ Add one or more symbols to the label. :param symbol: The integer symbol code as specified in the Hershey sign table. """ if hasattr(symbol, '__iter__'): for x in symbol: self._add_single_symbol(x) else: self._add_single_symbol(symbol)
[docs] def add_text(self, text, style=None): """ Add text to the label. The corresponding symbol codes are automatically looked in the specified lookup table. :param text: The text to be converted in geometric objects. :param style: The name of the lookup table. Defaults to the style passed during the creation of this object. """ style = style if style else self._style if style not in self._hershey_ascii_lookup_table: self._load_hersey_ascii_lookup_table(style) lookup_table = self._hershey_ascii_lookup_table[style]['from_ascii'] for char in str(text): if char == '\n': self._symbol_pos[0] = self._origin[0] self._symbol_pos[1] -= self._DEFAULT_CHAR_HEIGHT * self._scale continue ascii_value = ord(char) if ascii_value in lookup_table: self._add_single_symbol(lookup_table[ascii_value])
def __str__(self): text = '' for symbol in self._symbols: ascii_found = False for table in self._hershey_ascii_lookup_table.values(): if symbol in table['to_ascii']: ascii_found = True text += chr(table['to_ascii'][symbol]) break if not ascii_found: text += '{%i}' % symbol return "VectorText -\"{}\" layer={}".format(text, self.layer)