You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
498 lines
15 KiB
Python
498 lines
15 KiB
Python
"""
|
|
Functionality for reading/writing elements (geometry, text labels,
|
|
structure references) and associated properties.
|
|
"""
|
|
from typing import Optional, IO, TypeVar, Type, Union
|
|
from abc import ABCMeta, abstractmethod
|
|
from dataclasses import dataclass
|
|
|
|
import numpy
|
|
from numpy.typing import NDArray
|
|
|
|
from .basic import KlamathError
|
|
from .record import Record
|
|
|
|
from .records import BOX, BOUNDARY, NODE, PATH, TEXT, SREF, AREF
|
|
from .records import DATATYPE, PATHTYPE, BOXTYPE, NODETYPE, TEXTTYPE
|
|
from .records import LAYER, XY, WIDTH, COLROW, PRESENTATION, STRING
|
|
from .records import STRANS, MAG, ANGLE, PROPATTR, PROPVALUE
|
|
from .records import ENDEL, BGNEXTN, ENDEXTN, SNAME
|
|
|
|
|
|
E = TypeVar('E', bound='Element')
|
|
|
|
R = TypeVar('R', bound='Reference')
|
|
B = TypeVar('B', bound='Boundary')
|
|
P = TypeVar('P', bound='Path')
|
|
N = TypeVar('N', bound='Node')
|
|
T = TypeVar('T', bound='Text')
|
|
X = TypeVar('X', bound='Box')
|
|
|
|
|
|
def read_properties(stream: IO[bytes]) -> dict[int, bytes]:
|
|
"""
|
|
Read element properties.
|
|
|
|
Assumes PROPATTR records have unique values.
|
|
Stops reading after consuming ENDEL record.
|
|
|
|
Args:
|
|
stream: Stream to read from.
|
|
|
|
Returns:
|
|
{propattr: b'propvalue'} mapping.
|
|
"""
|
|
properties = {}
|
|
|
|
size, tag = Record.read_header(stream)
|
|
while tag != ENDEL.tag:
|
|
if tag == PROPATTR.tag:
|
|
key = PROPATTR.read_data(stream, size)[0]
|
|
value = PROPVALUE.read(stream)
|
|
if key in properties:
|
|
raise KlamathError(f'Duplicate property key: {key!r}')
|
|
properties[key] = value
|
|
size, tag = Record.read_header(stream)
|
|
return properties
|
|
|
|
|
|
def write_properties(stream: IO[bytes], properties: dict[int, bytes]) -> int:
|
|
"""
|
|
Write element properties.
|
|
|
|
This is does _not_ write the ENDEL record.
|
|
|
|
Args:
|
|
stream: Stream to write to.
|
|
"""
|
|
b = 0
|
|
for key, value in properties.items():
|
|
b += PROPATTR.write(stream, key)
|
|
b += PROPVALUE.write(stream, value)
|
|
return b
|
|
|
|
|
|
class Element(metaclass=ABCMeta):
|
|
"""
|
|
Abstract method definition for GDS structure contents
|
|
"""
|
|
@classmethod
|
|
@abstractmethod
|
|
def read(cls: Type[E], stream: IO[bytes]) -> E:
|
|
"""
|
|
Read from a stream to construct this object.
|
|
Consumes up to (and including) the ENDEL record.
|
|
|
|
Args:
|
|
Stream to read from.
|
|
|
|
Returns:
|
|
Constructed object.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
"""
|
|
Write this element to a stream.
|
|
Finishes with an ENDEL record.
|
|
|
|
Args:
|
|
Stream to write to.
|
|
|
|
Returns:
|
|
Number of bytes written
|
|
"""
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class Reference(Element):
|
|
"""
|
|
Datastructure representing
|
|
an instance of a structure (SREF / structure reference) or
|
|
an array of instances (AREF / array reference).
|
|
Type is determined by the presence of the `colrow` tuple.
|
|
|
|
Transforms are applied to each individual instance (_not_
|
|
to the instance's origin location or array vectors).
|
|
"""
|
|
__slots__ = ('struct_name', 'invert_y', 'mag', 'angle_deg', 'xy', 'colrow', 'properties')
|
|
|
|
struct_name: bytes
|
|
""" Name of the structure being referenced. """
|
|
|
|
invert_y: bool
|
|
""" Whether to mirror the pattern (negate y-values / flip across x-axis). Default False. """
|
|
|
|
mag: float
|
|
""" Scaling factor (default 1) """
|
|
|
|
angle_deg: float
|
|
""" Rotation (degrees counterclockwise) """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
"""
|
|
(For SREF) Location in the parent structure corresponding to the instance's origin (0, 0).
|
|
(For AREF) 3 locations:
|
|
[`offset`,
|
|
`offset + col_basis_vector * colrow[0]`,
|
|
`offset + row_basis_vector * colrow[1]`]
|
|
which define the first instance's offset and the array's basis vectors.
|
|
Note that many GDS implementations only support manhattan basis vectors, and some
|
|
assume a certain axis mapping (e.g. x->columns, y->rows) and "reinterpret" the
|
|
basis vectors to match it.
|
|
"""
|
|
|
|
colrow: tuple[int, int] | NDArray[numpy.int16] | None
|
|
""" Number of columns and rows (AREF) or None (SREF) """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties associated with this reference. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[R], stream: IO[bytes]) -> R:
|
|
invert_y = False
|
|
mag = 1
|
|
angle_deg = 0
|
|
colrow = None
|
|
struct_name = SNAME.skip_and_read(stream)
|
|
|
|
size, tag = Record.read_header(stream)
|
|
while tag != XY.tag:
|
|
if tag == STRANS.tag:
|
|
strans = STRANS.read_data(stream, size)
|
|
invert_y = bool(0x8000 & strans)
|
|
elif tag == MAG.tag:
|
|
mag = MAG.read_data(stream, size)[0]
|
|
elif tag == ANGLE.tag:
|
|
angle_deg = ANGLE.read_data(stream, size)[0]
|
|
elif tag == COLROW.tag:
|
|
colrow = COLROW.read_data(stream, size)
|
|
else:
|
|
raise KlamathError(f'Unexpected tag {tag:04x}')
|
|
size, tag = Record.read_header(stream)
|
|
xy = XY.read_data(stream, size).reshape(-1, 2)
|
|
properties = read_properties(stream)
|
|
return cls(
|
|
struct_name=struct_name,
|
|
xy=xy,
|
|
properties=properties,
|
|
colrow=colrow,
|
|
invert_y=invert_y,
|
|
mag=mag,
|
|
angle_deg=angle_deg,
|
|
)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = 0
|
|
if self.colrow is None:
|
|
b += SREF.write(stream, None)
|
|
else:
|
|
b += AREF.write(stream, None)
|
|
|
|
b += SNAME.write(stream, self.struct_name)
|
|
if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
|
|
b += STRANS.write(stream, int(self.invert_y) << 15)
|
|
if self.mag != 1:
|
|
b += MAG.write(stream, self.mag)
|
|
if self.angle_deg != 0:
|
|
b += ANGLE.write(stream, self.angle_deg)
|
|
|
|
if self.colrow is not None:
|
|
b += COLROW.write(stream, self.colrow)
|
|
|
|
b += XY.write(stream, self.xy)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|
|
|
|
def check(self) -> None:
|
|
if self.colrow is not None:
|
|
if self.xy.size != 6:
|
|
raise KlamathError(f'colrow is not None, so expected size-6 xy. Got {self.xy}')
|
|
else:
|
|
if self.xy.size != 2:
|
|
raise KlamathError(f'Expected size-2 xy. Got {self.xy}')
|
|
|
|
|
|
@dataclass
|
|
class Boundary(Element):
|
|
"""
|
|
Datastructure representing a Boundary element.
|
|
"""
|
|
__slots__ = ('layer', 'xy', 'properties')
|
|
|
|
layer: tuple[int, int]
|
|
""" (layer, data_type) tuple """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
""" Ordered vertices of the shape. First and last points should be identical. """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties for the element. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[B], stream: IO[bytes]) -> B:
|
|
layer = LAYER.skip_and_read(stream)[0]
|
|
dtype = DATATYPE.read(stream)[0]
|
|
xy = XY.read(stream).reshape(-1, 2)
|
|
properties = read_properties(stream)
|
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = BOUNDARY.write(stream, None)
|
|
b += LAYER.write(stream, self.layer[0])
|
|
b += DATATYPE.write(stream, self.layer[1])
|
|
b += XY.write(stream, self.xy)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|
|
|
|
|
|
@dataclass
|
|
class Path(Element):
|
|
"""
|
|
Datastructure representing a Path element.
|
|
|
|
If `path_type < 4`, `extension` values are not written.
|
|
During read, `exension` defaults to (0, 0) even if unused.
|
|
"""
|
|
__slots__ = ('layer', 'xy', 'properties', 'path_type', 'width', 'extension')
|
|
|
|
layer: tuple[int, int]
|
|
""" (layer, data_type) tuple """
|
|
|
|
path_type: int
|
|
""" End-cap type (0: flush, 1: circle, 2: square, 4: custom) """
|
|
|
|
width: int
|
|
""" Path width """
|
|
|
|
extension: tuple[int, int]
|
|
""" Extension when using path_type=4. Ignored otherwise. """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
""" Path centerline coordinates """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties for the element. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[P], stream: IO[bytes]) -> P:
|
|
path_type = 0
|
|
width = 0
|
|
bgn_ext = 0
|
|
end_ext = 0
|
|
layer = LAYER.skip_and_read(stream)[0]
|
|
dtype = DATATYPE.read(stream)[0]
|
|
|
|
size, tag = Record.read_header(stream)
|
|
while tag != XY.tag:
|
|
if tag == PATHTYPE.tag:
|
|
path_type = PATHTYPE.read_data(stream, size)[0]
|
|
elif tag == WIDTH.tag:
|
|
width = WIDTH.read_data(stream, size)[0]
|
|
elif tag == BGNEXTN.tag:
|
|
bgn_ext = BGNEXTN.read_data(stream, size)[0]
|
|
elif tag == ENDEXTN.tag:
|
|
end_ext = ENDEXTN.read_data(stream, size)[0]
|
|
else:
|
|
raise KlamathError(f'Unexpected tag {tag:04x}')
|
|
size, tag = Record.read_header(stream)
|
|
xy = XY.read_data(stream, size).reshape(-1, 2)
|
|
properties = read_properties(stream)
|
|
return cls(layer=(layer, dtype), xy=xy,
|
|
properties=properties, extension=(bgn_ext, end_ext),
|
|
path_type=path_type, width=width)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = PATH.write(stream, None)
|
|
b += LAYER.write(stream, self.layer[0])
|
|
b += DATATYPE.write(stream, self.layer[1])
|
|
if self.path_type != 0:
|
|
b += PATHTYPE.write(stream, self.path_type)
|
|
if self.width != 0:
|
|
b += WIDTH.write(stream, self.width)
|
|
|
|
if self.path_type < 4:
|
|
bgn_ext, end_ext = self.extension
|
|
if bgn_ext != 0:
|
|
b += BGNEXTN.write(stream, bgn_ext)
|
|
if end_ext != 0:
|
|
b += ENDEXTN.write(stream, end_ext)
|
|
b += XY.write(stream, self.xy)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|
|
|
|
|
|
@dataclass
|
|
class Box(Element):
|
|
"""
|
|
Datastructure representing a Box element. Rarely used.
|
|
"""
|
|
__slots__ = ('layer', 'xy', 'properties')
|
|
|
|
layer: tuple[int, int]
|
|
""" (layer, box_type) tuple """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
""" Box coordinates (5 pairs) """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties for the element. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[X], stream: IO[bytes]) -> X:
|
|
layer = LAYER.skip_and_read(stream)[0]
|
|
dtype = BOXTYPE.read(stream)[0]
|
|
xy = XY.read(stream).reshape(-1, 2)
|
|
properties = read_properties(stream)
|
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = BOX.write(stream, None)
|
|
b += LAYER.write(stream, self.layer[0])
|
|
b += BOXTYPE.write(stream, self.layer[1])
|
|
b += XY.write(stream, self.xy)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|
|
|
|
|
|
@dataclass
|
|
class Node(Element):
|
|
"""
|
|
Datastructure representing a Node element. Rarely used.
|
|
"""
|
|
__slots__ = ('layer', 'xy', 'properties')
|
|
|
|
layer: tuple[int, int]
|
|
""" (layer, node_type) tuple """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
""" 1-50 pairs of coordinates. """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties for the element. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[N], stream: IO[bytes]) -> N:
|
|
layer = LAYER.skip_and_read(stream)[0]
|
|
dtype = NODETYPE.read(stream)[0]
|
|
xy = XY.read(stream).reshape(-1, 2)
|
|
properties = read_properties(stream)
|
|
return cls(layer=(layer, dtype), xy=xy, properties=properties)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = NODE.write(stream, None)
|
|
b += LAYER.write(stream, self.layer[0])
|
|
b += NODETYPE.write(stream, self.layer[1])
|
|
b += XY.write(stream, self.xy)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|
|
|
|
|
|
@dataclass
|
|
class Text(Element):
|
|
"""
|
|
Datastructure representing a text label.
|
|
"""
|
|
__slots__ = ('layer', 'xy', 'properties', 'presentation', 'path_type',
|
|
'width', 'invert_y', 'mag', 'angle_deg', 'string')
|
|
|
|
layer: tuple[int, int]
|
|
""" (layer, node_type) tuple """
|
|
|
|
presentation: int
|
|
""" Bit array. Default all zeros.
|
|
bits 0-1: 00 left/01 center/10 right
|
|
bits 2-3: 00 top/01 middle/10 bottom
|
|
bits 4-5: font number
|
|
"""
|
|
|
|
path_type: int
|
|
""" Default 0 """
|
|
|
|
width: int
|
|
""" Default 0 """
|
|
|
|
invert_y: bool
|
|
""" Vertical inversion. Default False. """
|
|
|
|
mag: float
|
|
""" Scaling factor. Default 1. """
|
|
|
|
angle_deg: float
|
|
""" Rotation (ccw). Default 0. """
|
|
|
|
xy: NDArray[numpy.int32]
|
|
""" Position (1 pair only) """
|
|
|
|
string: bytes
|
|
""" Text content """
|
|
|
|
properties: dict[int, bytes]
|
|
""" Properties for the element. """
|
|
|
|
@classmethod
|
|
def read(cls: Type[T], stream: IO[bytes]) -> T:
|
|
path_type = 0
|
|
presentation = 0
|
|
invert_y = False
|
|
width = 0
|
|
mag = 1
|
|
angle_deg = 0
|
|
layer = LAYER.skip_and_read(stream)[0]
|
|
dtype = TEXTTYPE.read(stream)[0]
|
|
|
|
size, tag = Record.read_header(stream)
|
|
while tag != XY.tag:
|
|
if tag == PRESENTATION.tag:
|
|
presentation = PRESENTATION.read_data(stream, size)
|
|
elif tag == PATHTYPE.tag:
|
|
path_type = PATHTYPE.read_data(stream, size)[0]
|
|
elif tag == WIDTH.tag:
|
|
width = WIDTH.read_data(stream, size)[0]
|
|
elif tag == STRANS.tag:
|
|
strans = STRANS.read_data(stream, size)
|
|
invert_y = bool(0x8000 & strans)
|
|
elif tag == MAG.tag:
|
|
mag = MAG.read_data(stream, size)[0]
|
|
elif tag == ANGLE.tag:
|
|
angle_deg = ANGLE.read_data(stream, size)[0]
|
|
else:
|
|
raise KlamathError(f'Unexpected tag {tag:04x}')
|
|
size, tag = Record.read_header(stream)
|
|
xy = XY.read_data(stream, size).reshape(-1, 2)
|
|
|
|
string = STRING.read(stream)
|
|
properties = read_properties(stream)
|
|
return cls(layer=(layer, dtype), xy=xy, properties=properties,
|
|
string=string, presentation=presentation, path_type=path_type,
|
|
width=width, invert_y=invert_y, mag=mag, angle_deg=angle_deg)
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
b = TEXT.write(stream, None)
|
|
b += LAYER.write(stream, self.layer[0])
|
|
b += TEXTTYPE.write(stream, self.layer[1])
|
|
if self.presentation != 0:
|
|
b += PRESENTATION.write(stream, self.presentation)
|
|
if self.path_type != 0:
|
|
b += PATHTYPE.write(stream, self.path_type)
|
|
if self.width != 0:
|
|
b += WIDTH.write(stream, self.width)
|
|
if self.angle_deg != 0 or self.mag != 1 or self.invert_y:
|
|
b += STRANS.write(stream, int(self.invert_y) << 15)
|
|
if self.mag != 1:
|
|
b += MAG.write(stream, self.mag)
|
|
if self.angle_deg != 0:
|
|
b += ANGLE.write(stream, self.angle_deg)
|
|
b += XY.write(stream, self.xy)
|
|
b += STRING.write(stream, self.string)
|
|
b += write_properties(stream, self.properties)
|
|
b += ENDEL.write(stream, None)
|
|
return b
|