move to dicty layers and targets
This commit is contained in:
parent
dc4f24a028
commit
88d123af71
@ -1,4 +1,4 @@
|
||||
from typing import Self, Sequence, Mapping, Literal, overload, Final, cast
|
||||
from typing import Self, Sequence, Mapping, Literal, overload
|
||||
import copy
|
||||
import logging
|
||||
|
||||
@ -482,10 +482,10 @@ class Builder(PortList):
|
||||
self.pattern.append(other_copy)
|
||||
else:
|
||||
assert not isinstance(other, Pattern)
|
||||
ref = Ref(other.name, mirrored=mirrored)
|
||||
ref = Ref(mirrored=mirrored)
|
||||
ref.rotate_around(pivot, rotation)
|
||||
ref.translate(offset)
|
||||
self.pattern.refs.append(ref)
|
||||
self.pattern.refs[other.name].append(ref)
|
||||
return self
|
||||
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
|
@ -279,10 +279,10 @@ class RenderPather(PortList):
|
||||
p.translate(offset)
|
||||
self.ports[name] = p
|
||||
|
||||
sp = Ref(other.name, mirrored=mirrored)
|
||||
sp.rotate_around(pivot, rotation)
|
||||
sp.translate(offset)
|
||||
self.pattern.refs.append(sp)
|
||||
ref = Ref(mirrored=mirrored)
|
||||
ref.rotate_around(pivot, rotation)
|
||||
ref.translate(offset)
|
||||
self.pattern.refs[other.name].append(ref)
|
||||
return self
|
||||
|
||||
def path(
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides)
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Sequence, Literal, Callable
|
||||
from typing import Sequence, Literal, Callable
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
@ -20,6 +20,7 @@ render_step_t = (
|
||||
| tuple[Literal['P'], None, float, float, str, None]
|
||||
)
|
||||
|
||||
|
||||
class Tool:
|
||||
def path(
|
||||
self,
|
||||
@ -153,6 +154,4 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
if out_transition:
|
||||
bb.plug(opat, {port_names[1]: oport_ours})
|
||||
|
||||
|
||||
return bb.pattern
|
||||
|
||||
|
@ -219,7 +219,7 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
|
||||
if points.shape[1] == 2:
|
||||
raise PatternError('Invalid or unimplemented polygon?')
|
||||
#shape = Polygon(layer=layer)
|
||||
#shape = Polygon()
|
||||
elif points.shape[1] > 2:
|
||||
if (points[0, 2] != points[:, 2]).any():
|
||||
raise PatternError('PolyLine has non-constant width (not yet representable in masque!)')
|
||||
@ -232,11 +232,11 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
|
||||
shape: Path | Polygon
|
||||
if width == 0 and len(points) > 2 and numpy.array_equal(points[0], points[-1]):
|
||||
shape = Polygon(layer=layer, vertices=points[:-1, :2])
|
||||
shape = Polygon(vertices=points[:-1, :2])
|
||||
else:
|
||||
shape = Path(layer=layer, width=width, vertices=points[:, :2])
|
||||
shape = Path(width=width, vertices=points[:, :2])
|
||||
|
||||
pat.shapes.append(shape)
|
||||
pat.shapes[layer].append(shape)
|
||||
|
||||
elif eltype in ('TEXT',):
|
||||
args = dict(
|
||||
@ -248,9 +248,9 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
# if height != 0:
|
||||
# logger.warning('Interpreting DXF TEXT as a label despite nonzero height. '
|
||||
# 'This could be changed in the future by setting a font path in the masque DXF code.')
|
||||
pat.labels.append(Label(string=string, **args))
|
||||
pat.label(string=string, **args)
|
||||
# else:
|
||||
# pat.shapes.append(Text(string=string, height=height, font_path=????))
|
||||
# pat.shapes[args['layer']].append(Text(string=string, height=height, font_path=????))
|
||||
elif eltype in ('INSERT',):
|
||||
attr = element.dxfattribs()
|
||||
xscale = attr.get('xscale', 1)
|
||||
@ -286,13 +286,9 @@ def _read_block(block) -> tuple[str, Pattern]:
|
||||
|
||||
def _mrefs_to_drefs(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
refs: list[Ref],
|
||||
refs: dict[str | None, list[Ref]],
|
||||
) -> None:
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
continue
|
||||
encoded_name = ref.target
|
||||
|
||||
def mk_blockref(encoded_name: str, ref: Ref) -> None:
|
||||
rotation = numpy.rad2deg(ref.rotation) % 360
|
||||
attribs = dict(
|
||||
xscale=ref.scale * (-1 if ref.mirrored[1] else 1),
|
||||
@ -330,36 +326,47 @@ def _mrefs_to_drefs(
|
||||
for dd in rep.displacements:
|
||||
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
for ref in rseq:
|
||||
mk_blockref(target, ref)
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
shapes: list[Shape],
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
polygonize_paths: bool = False,
|
||||
) -> None:
|
||||
# Add `LWPolyline`s for each shape.
|
||||
# Could set do paths with width setting, but need to consider endcaps.
|
||||
for shape in shapes:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError(
|
||||
'Shape repetitions are not supported by DXF.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.'
|
||||
)
|
||||
for layer, sseq in shapes.items():
|
||||
attribs = dict(layer=_mlayer2dxf(layer))
|
||||
for shape in sseq:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError(
|
||||
'Shape repetitions are not supported by DXF.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.'
|
||||
)
|
||||
|
||||
attribs = dict(layer=_mlayer2dxf(shape.layer))
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
|
||||
|
||||
def _labels_to_texts(
|
||||
block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace,
|
||||
labels: list[Label],
|
||||
labels: dict[layer_t, list[Label]],
|
||||
) -> None:
|
||||
for label in labels:
|
||||
attribs = dict(layer=_mlayer2dxf(label.layer))
|
||||
xy = label.offset
|
||||
block.add_text(label.string, dxfattribs=attribs).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
for layer, lseq in labels.items():
|
||||
attribs = dict(layer=_mlayer2dxf(layer))
|
||||
for label in lseq:
|
||||
xy = label.offset
|
||||
block.add_text(
|
||||
label.string,
|
||||
dxfattribs=attribs
|
||||
).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||
|
||||
|
||||
def _mlayer2dxf(layer: layer_t) -> str:
|
||||
|
@ -253,21 +253,21 @@ def read_elements(
|
||||
elements = klamath.library.read_elements(stream)
|
||||
for element in elements:
|
||||
if isinstance(element, klamath.elements.Boundary):
|
||||
poly = _boundary_to_polygon(element, raw_mode)
|
||||
pat.shapes.append(poly)
|
||||
layer, poly = _boundary_to_polygon(element, raw_mode)
|
||||
pat.shapes[layer].append(poly)
|
||||
elif isinstance(element, klamath.elements.Path):
|
||||
path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes.append(path)
|
||||
layer, path = _gpath_to_mpath(element, raw_mode)
|
||||
pat.shapes[layer].append(path)
|
||||
elif isinstance(element, klamath.elements.Text):
|
||||
label = Label(
|
||||
offset=element.xy.astype(float),
|
||||
pat.label(
|
||||
layer=element.layer,
|
||||
offset=element.xy.astype(float),
|
||||
string=element.string.decode('ASCII'),
|
||||
annotations=_properties_to_annotations(element.properties),
|
||||
)
|
||||
pat.labels.append(label)
|
||||
elif isinstance(element, klamath.elements.Reference):
|
||||
pat.refs.append(_gref_to_mref(element))
|
||||
target, ref = _gref_to_mref(element)
|
||||
pat.refs[target].append(ref)
|
||||
return pat
|
||||
|
||||
|
||||
@ -287,7 +287,7 @@ def _mlayer2gds(mlayer: layer_t) -> tuple[int, int]:
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||
def _gref_to_mref(ref: klamath.library.Reference) -> tuple[str, Ref]:
|
||||
"""
|
||||
Helper function to create a Ref from an SREF or AREF. Sets ref.target to struct_name.
|
||||
"""
|
||||
@ -301,8 +301,8 @@ def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||
a_count=a_count, b_count=b_count)
|
||||
|
||||
target = ref.struct_name.decode('ASCII')
|
||||
mref = Ref(
|
||||
target=ref.struct_name.decode('ASCII'),
|
||||
offset=offset,
|
||||
rotation=numpy.deg2rad(ref.angle_deg),
|
||||
scale=ref.mag,
|
||||
@ -310,10 +310,10 @@ def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||
annotations=_properties_to_annotations(ref.properties),
|
||||
repetition=repetition,
|
||||
)
|
||||
return mref
|
||||
return target, mref
|
||||
|
||||
|
||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> tuple[layer_t, Path]:
|
||||
if gpath.path_type in path_cap_map:
|
||||
cap = path_cap_map[gpath.path_type]
|
||||
else:
|
||||
@ -321,7 +321,6 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||
|
||||
mpath = Path(
|
||||
vertices=gpath.xy.astype(float),
|
||||
layer=gpath.layer,
|
||||
width=gpath.width,
|
||||
cap=cap,
|
||||
offset=numpy.zeros(2),
|
||||
@ -330,74 +329,73 @@ def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||
)
|
||||
if cap == Path.Cap.SquareCustom:
|
||||
mpath.cap_extensions = gpath.extension
|
||||
return mpath
|
||||
return gpath.layer, mpath
|
||||
|
||||
|
||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> Polygon:
|
||||
return Polygon(
|
||||
def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) -> tuple[layer_t, Polygon]:
|
||||
return boundary.layer, Polygon(
|
||||
vertices=boundary.xy[:-1].astype(float),
|
||||
layer=boundary.layer,
|
||||
offset=numpy.zeros(2),
|
||||
annotations=_properties_to_annotations(boundary.properties),
|
||||
raw=raw_mode,
|
||||
)
|
||||
|
||||
|
||||
def _mrefs_to_grefs(refs: list[Ref]) -> list[klamath.library.Reference]:
|
||||
def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.Reference]:
|
||||
grefs = []
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
encoded_name = ref.target.encode('ASCII')
|
||||
encoded_name = target.encode('ASCII')
|
||||
for ref in rseq:
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
rep = ref.repetition
|
||||
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
properties = _annotations_to_properties(ref.annotations, 512)
|
||||
|
||||
# Note: GDS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
rep = ref.repetition
|
||||
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
properties = _annotations_to_properties(ref.annotations, 512)
|
||||
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy = numpy.array(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
])
|
||||
aref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast(xy),
|
||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(aref)
|
||||
elif rep is None:
|
||||
sref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(sref)
|
||||
else:
|
||||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
if isinstance(rep, Grid):
|
||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||
b_count = rep.b_count if rep.b_count is not None else 1
|
||||
xy = numpy.array(ref.offset) + numpy.array([
|
||||
[0.0, 0.0],
|
||||
rep.a_vector * rep.a_count,
|
||||
b_vector * b_count,
|
||||
])
|
||||
aref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset + dd]),
|
||||
xy=rint_cast(xy),
|
||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
grefs.append(aref)
|
||||
elif rep is None:
|
||||
sref = klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
grefs += new_srefs
|
||||
grefs.append(sref)
|
||||
else:
|
||||
new_srefs = [
|
||||
klamath.library.Reference(
|
||||
struct_name=encoded_name,
|
||||
xy=rint_cast([ref.offset + dd]),
|
||||
colrow=None,
|
||||
angle_deg=angle_deg,
|
||||
invert_y=mirror_across_x,
|
||||
mag=ref.scale,
|
||||
properties=properties,
|
||||
)
|
||||
for dd in rep.displacements]
|
||||
grefs += new_srefs
|
||||
return grefs
|
||||
|
||||
|
||||
@ -428,51 +426,41 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
shapes: list[Shape],
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
polygonize_paths: bool = False,
|
||||
) -> list[klamath.elements.Element]:
|
||||
elements: list[klamath.elements.Element] = []
|
||||
# Add a Boundary element for each shape, and Path elements if necessary
|
||||
for shape in shapes:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError('Shape repetitions are not supported by GDS.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.')
|
||||
for mlayer, sseq in shapes.items():
|
||||
layer, data_type = _mlayer2gds(mlayer)
|
||||
for shape in sseq:
|
||||
if shape.repetition is not None:
|
||||
raise PatternError('Shape repetitions are not supported by GDS.'
|
||||
' Please call library.wrap_repeated_shapes() before writing to file.')
|
||||
|
||||
layer, data_type = _mlayer2gds(shape.layer)
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
xy = rint_cast(shape.vertices + shape.offset)
|
||||
width = rint_cast(shape.width)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
properties = _annotations_to_properties(shape.annotations, 128)
|
||||
if isinstance(shape, Path) and not polygonize_paths:
|
||||
xy = rint_cast(shape.vertices + shape.offset)
|
||||
width = rint_cast(shape.width)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
|
||||
extension: tuple[int, int]
|
||||
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
|
||||
extension = tuple(shape.cap_extensions) # type: ignore
|
||||
else:
|
||||
extension = (0, 0)
|
||||
extension: tuple[int, int]
|
||||
if shape.cap == Path.Cap.SquareCustom and shape.cap_extensions is not None:
|
||||
extension = tuple(shape.cap_extensions) # type: ignore
|
||||
else:
|
||||
extension = (0, 0)
|
||||
|
||||
path = klamath.elements.Path(
|
||||
layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=int(width),
|
||||
extension=extension,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(path)
|
||||
elif isinstance(shape, Polygon):
|
||||
polygon = shape
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
path = klamath.elements.Path(
|
||||
layer=(layer, data_type),
|
||||
xy=xy,
|
||||
path_type=path_type,
|
||||
width=int(width),
|
||||
extension=extension,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(path)
|
||||
elif isinstance(shape, Polygon):
|
||||
polygon = shape
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
@ -482,28 +470,40 @@ def _shapes_to_elements(
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
||||
xy_closed[-1] = xy_closed[0]
|
||||
boundary = klamath.elements.Boundary(
|
||||
layer=(layer, data_type),
|
||||
xy=xy_closed,
|
||||
properties=properties,
|
||||
)
|
||||
elements.append(boundary)
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(labels: list[Label]) -> list[klamath.elements.Text]:
|
||||
def _labels_to_texts(labels: dict[layer_t, list[Label]]) -> list[klamath.elements.Text]:
|
||||
texts = []
|
||||
for label in labels:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
layer, text_type = _mlayer2gds(label.layer)
|
||||
xy = rint_cast([label.offset])
|
||||
text = klamath.elements.Text(
|
||||
layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # TODO maybe set some of these?
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1,
|
||||
)
|
||||
texts.append(text)
|
||||
for mlayer, lseq in labels.items():
|
||||
layer, text_type = _mlayer2gds(mlayer)
|
||||
for label in lseq:
|
||||
properties = _annotations_to_properties(label.annotations, 128)
|
||||
xy = rint_cast([label.offset])
|
||||
text = klamath.elements.Text(
|
||||
layer=(layer, text_type),
|
||||
xy=xy,
|
||||
string=label.string.encode('ASCII'),
|
||||
properties=properties,
|
||||
presentation=0, # TODO maybe set some of these?
|
||||
angle_deg=0,
|
||||
invert_y=False,
|
||||
width=0,
|
||||
path_type=0,
|
||||
mag=1,
|
||||
)
|
||||
texts.append(text)
|
||||
return texts
|
||||
|
||||
|
||||
|
@ -30,7 +30,7 @@ from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringR
|
||||
from .utils import is_gzipped, tmpfile
|
||||
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
|
||||
from ..library import Library, ILibrary
|
||||
from ..shapes import Polygon, Path, Circle
|
||||
from ..shapes import Path, Circle
|
||||
from ..repetition import Grid, Arbitrary, Repetition
|
||||
from ..utils import layer_t, normalize_mirror, annotations_t
|
||||
|
||||
@ -284,16 +284,13 @@ def read(
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list()[:-1])), axis=0)
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
poly = Polygon(
|
||||
pat.polygon(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
annotations=annotations,
|
||||
repetition=repetition,
|
||||
)
|
||||
|
||||
pat.shapes.append(poly)
|
||||
|
||||
elif isinstance(element, fatrec.Path):
|
||||
vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0)
|
||||
|
||||
@ -311,7 +308,7 @@ def read(
|
||||
))
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
path = Path(
|
||||
pat.path(
|
||||
vertices=vertices,
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
@ -322,20 +319,17 @@ def read(
|
||||
**path_args,
|
||||
)
|
||||
|
||||
pat.shapes.append(path)
|
||||
|
||||
elif isinstance(element, fatrec.Rectangle):
|
||||
width = element.get_width()
|
||||
height = element.get_height()
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
rect = Polygon(
|
||||
pat.polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height),
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(rect)
|
||||
|
||||
elif isinstance(element, fatrec.Trapezoid):
|
||||
vertices = numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (element.get_width(), element.get_height())
|
||||
@ -363,14 +357,13 @@ def read(
|
||||
vertices[2, 0] -= b
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
trapz = Polygon(
|
||||
pat.polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=vertices,
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(trapz)
|
||||
|
||||
elif isinstance(element, fatrec.CTrapezoid):
|
||||
cttype = element.get_ctrapezoid_type()
|
||||
@ -419,25 +412,24 @@ def read(
|
||||
vertices[0, 1] += width
|
||||
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
ctrapz = Polygon(
|
||||
pat.polygon(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
vertices=vertices,
|
||||
annotations=annotations,
|
||||
)
|
||||
pat.shapes.append(ctrapz)
|
||||
|
||||
elif isinstance(element, fatrec.Circle):
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
layer = element.get_layer_tuple()
|
||||
circle = Circle(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
radius=float(element.get_radius()),
|
||||
)
|
||||
pat.shapes.append(circle)
|
||||
pat.shapes[layer].append(circle)
|
||||
|
||||
elif isinstance(element, fatrec.Text):
|
||||
annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings)
|
||||
@ -446,21 +438,21 @@ def read(
|
||||
string = lib.textstrings[str_or_ref].string
|
||||
else:
|
||||
string = str_or_ref.string
|
||||
label = Label(
|
||||
pat.label(
|
||||
layer=element.get_layer_tuple(),
|
||||
offset=element.get_xy(),
|
||||
repetition=repetition,
|
||||
annotations=annotations,
|
||||
string=string,
|
||||
)
|
||||
pat.labels.append(label)
|
||||
|
||||
else:
|
||||
logger.warning(f'Skipping record {element} (unimplemented)')
|
||||
continue
|
||||
|
||||
for placement in cell.placements:
|
||||
pat.refs.append(_placement_to_ref(placement, lib))
|
||||
target, ref = _placement_to_ref(placement, lib)
|
||||
pat.refs[target].append(ref)
|
||||
|
||||
mlib[cell_name] = pat
|
||||
|
||||
@ -484,9 +476,9 @@ def _mlayer2oas(mlayer: layer_t) -> tuple[int, int]:
|
||||
return layer, data_type
|
||||
|
||||
|
||||
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> Ref:
|
||||
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> tuple[int | str, Ref]:
|
||||
"""
|
||||
Helper function to create a Ref from a placment. Sets ref.target to the placement name.
|
||||
Helper function to create a Ref from a placment. Also returns the placement name (or id).
|
||||
"""
|
||||
assert not isinstance(placement.repetition, fatamorgana.ReuseRepetition)
|
||||
xy = numpy.array((placement.x, placement.y))
|
||||
@ -501,7 +493,6 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
|
||||
else:
|
||||
rotation = numpy.deg2rad(float(placement.angle))
|
||||
ref = Ref(
|
||||
target=name,
|
||||
offset=xy,
|
||||
mirrored=(placement.flip, False),
|
||||
rotation=rotation,
|
||||
@ -509,116 +500,118 @@ def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout)
|
||||
repetition=repetition_fata2masq(placement.repetition),
|
||||
annotations=annotations,
|
||||
)
|
||||
return ref
|
||||
return name, ref
|
||||
|
||||
|
||||
def _refs_to_placements(
|
||||
refs: list[Ref],
|
||||
refs: dict[str | None, list[Ref]],
|
||||
) -> list[fatrec.Placement]:
|
||||
placements = []
|
||||
for ref in refs:
|
||||
if ref.target is None:
|
||||
for target, rseq in refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
for ref in rseq:
|
||||
# Note: OASIS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||
|
||||
# Note: OASIS mirrors first and rotates second
|
||||
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||
offset = rint_cast(ref.offset + rep_offset)
|
||||
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
placement = fatrec.Placement(
|
||||
name=target,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=ref.scale,
|
||||
properties=annotations_to_properties(ref.annotations),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
repetition=frep,
|
||||
)
|
||||
|
||||
offset = rint_cast(ref.offset + rep_offset)
|
||||
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||
placement = fatrec.Placement(
|
||||
name=ref.target,
|
||||
flip=mirror_across_x,
|
||||
angle=angle,
|
||||
magnification=ref.scale,
|
||||
properties=annotations_to_properties(ref.annotations),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
repetition=frep,
|
||||
)
|
||||
|
||||
placements.append(placement)
|
||||
placements.append(placement)
|
||||
return placements
|
||||
|
||||
|
||||
def _shapes_to_elements(
|
||||
shapes: list[Shape],
|
||||
shapes: dict[layer_t, list[Shape]],
|
||||
layer2oas: Callable[[layer_t], tuple[int, int]],
|
||||
) -> list[fatrec.Polygon | fatrec.Path | fatrec.Circle]:
|
||||
# Add a Polygon record for each shape, and Path elements if necessary
|
||||
elements: list[fatrec.Polygon | fatrec.Path | fatrec.Circle] = []
|
||||
for shape in shapes:
|
||||
layer, datatype = layer2oas(shape.layer)
|
||||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||
properties = annotations_to_properties(shape.annotations)
|
||||
if isinstance(shape, Circle):
|
||||
offset = rint_cast(shape.offset + rep_offset)
|
||||
radius = rint_cast(shape.radius)
|
||||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||
half_width = rint_cast(shape.width / 2)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||
elements.append(fatrec.Polygon(
|
||||
for mlayer, sseq in shapes.items():
|
||||
layer, datatype = layer2oas(mlayer)
|
||||
for shape in sseq:
|
||||
repetition, rep_offset = repetition_masq2fata(shape.repetition)
|
||||
properties = annotations_to_properties(shape.annotations)
|
||||
if isinstance(shape, Circle):
|
||||
offset = rint_cast(shape.offset + rep_offset)
|
||||
radius = rint_cast(shape.radius)
|
||||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=cast(list[list[int]], points),
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
)
|
||||
elements.append(circle)
|
||||
elif isinstance(shape, Path):
|
||||
xy = rint_cast(shape.offset + shape.vertices[0] + rep_offset)
|
||||
deltas = rint_cast(numpy.diff(shape.vertices, axis=0))
|
||||
half_width = rint_cast(shape.width / 2)
|
||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
||||
extension_start = (path_type, shape.cap_extensions[0] if shape.cap_extensions is not None else None)
|
||||
extension_end = (path_type, shape.cap_extensions[1] if shape.cap_extensions is not None else None)
|
||||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
extension_end=extension_end,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
)
|
||||
elements.append(path)
|
||||
else:
|
||||
for polygon in shape.to_polygons():
|
||||
xy = rint_cast(polygon.offset + polygon.vertices[0] + rep_offset)
|
||||
points = rint_cast(numpy.diff(polygon.vertices, axis=0))
|
||||
elements.append(fatrec.Polygon(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=cast(list[list[int]], points),
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
return elements
|
||||
|
||||
|
||||
def _labels_to_texts(
|
||||
labels: list[Label],
|
||||
labels: dict[layer_t, list[Label]],
|
||||
layer2oas: Callable[[layer_t], tuple[int, int]],
|
||||
) -> list[fatrec.Text]:
|
||||
texts = []
|
||||
for label in labels:
|
||||
layer, datatype = layer2oas(label.layer)
|
||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||
xy = rint_cast(label.offset + rep_offset)
|
||||
properties = annotations_to_properties(label.annotations)
|
||||
texts.append(fatrec.Text(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
for mlayer, lseq in labels.items():
|
||||
layer, datatype = layer2oas(mlayer)
|
||||
for label in lseq:
|
||||
repetition, rep_offset = repetition_masq2fata(label.repetition)
|
||||
xy = rint_cast(label.offset + rep_offset)
|
||||
properties = annotations_to_properties(label.annotations)
|
||||
texts.append(fatrec.Text(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
string=label.string,
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
return texts
|
||||
|
||||
|
||||
|
@ -65,22 +65,24 @@ def writefile(
|
||||
for name, pat in library.items():
|
||||
svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red')
|
||||
|
||||
for shape in pat.shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec = poly2path(polygon.vertices + polygon.offset)
|
||||
for layer, shapes in pat.shapes.items():
|
||||
for shape in shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec = poly2path(polygon.vertices + polygon.offset)
|
||||
|
||||
path = svg.path(d=path_spec)
|
||||
if custom_attributes:
|
||||
path['pattern_layer'] = polygon.layer
|
||||
path = svg.path(d=path_spec)
|
||||
if custom_attributes:
|
||||
path['pattern_layer'] = layer
|
||||
|
||||
svg_group.add(path)
|
||||
svg_group.add(path)
|
||||
|
||||
for ref in pat.refs:
|
||||
if ref.target is None:
|
||||
for target, refs in pat.refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(ref.target), transform=transform)
|
||||
svg_group.add(use)
|
||||
for ref in refs:
|
||||
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||
use = svg.use(href='#' + mangle_name(target), transform=transform)
|
||||
svg_group.add(use)
|
||||
|
||||
svg.defs.add(svg_group)
|
||||
svg.add(svg.use(href='#' + mangle_name(top)))
|
||||
@ -133,9 +135,10 @@ def writefile_inverted(
|
||||
path_spec = poly2path(slab_edge)
|
||||
|
||||
# Draw polygons with reversed vertex order
|
||||
for shape in pattern.shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
|
||||
for _layer, shapes in pattern.shapes.items():
|
||||
for shape in shapes:
|
||||
for polygon in shape.to_polygons():
|
||||
path_spec += poly2path(polygon.vertices[::-1] + polygon.offset)
|
||||
|
||||
svg.add(svg.path(d=path_spec, fill='blue', stroke='red'))
|
||||
svg.save()
|
||||
|
@ -42,16 +42,17 @@ def clean_pattern_vertices(pat: Pattern) -> Pattern:
|
||||
Returns:
|
||||
pat
|
||||
"""
|
||||
remove_inds = []
|
||||
for ii, shape in enumerate(pat.shapes):
|
||||
if not isinstance(shape, (Polygon, Path)):
|
||||
continue
|
||||
try:
|
||||
shape.clean_vertices()
|
||||
except PatternError:
|
||||
remove_inds.append(ii)
|
||||
for ii in sorted(remove_inds, reverse=True):
|
||||
del pat.shapes[ii]
|
||||
for shapes in pat.shapes.values():
|
||||
remove_inds = []
|
||||
for ii, shape in enumerate(shapes):
|
||||
if not isinstance(shape, (Polygon, Path)):
|
||||
continue
|
||||
try:
|
||||
shape.clean_vertices()
|
||||
except PatternError:
|
||||
remove_inds.append(ii)
|
||||
for ii in sorted(remove_inds, reverse=True):
|
||||
del shapes[ii]
|
||||
return pat
|
||||
|
||||
|
||||
|
@ -5,15 +5,15 @@ import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .repetition import Repetition
|
||||
from .utils import rotation_matrix_2d, layer_t, AutoSlots, annotations_t
|
||||
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, RepeatableImpl
|
||||
from .utils import rotation_matrix_2d, AutoSlots, annotations_t
|
||||
from .traits import PositionableImpl, Copyable, Pivotable, RepeatableImpl, Bounded
|
||||
from .traits import AnnotatableImpl
|
||||
|
||||
|
||||
class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
Pivotable, Copyable, metaclass=AutoSlots):
|
||||
class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
Bounded, Pivotable, Copyable, metaclass=AutoSlots):
|
||||
"""
|
||||
A text annotation with a position and layer (but no size; it is not drawn)
|
||||
A text annotation with a position (but no size; it is not drawn)
|
||||
"""
|
||||
__slots__ = ( '_string', )
|
||||
|
||||
@ -40,13 +40,11 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
string: str,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
) -> None:
|
||||
self.string = string
|
||||
self.offset = numpy.array(offset, dtype=float, copy=True)
|
||||
self.layer = layer
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
@ -54,7 +52,6 @@ class Label(PositionableImpl, LayerableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
return type(self)(
|
||||
string=self.string,
|
||||
offset=self.offset.copy(),
|
||||
layer=self.layer,
|
||||
repetition=self.repetition,
|
||||
)
|
||||
|
||||
|
@ -226,11 +226,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
flattened[name] = None
|
||||
pat = self[name].deepcopy()
|
||||
|
||||
for ref in pat.refs:
|
||||
target = ref.target
|
||||
for target in pat.refs:
|
||||
if target is None:
|
||||
continue
|
||||
|
||||
if target not in flattened:
|
||||
flatten_single(target)
|
||||
|
||||
@ -240,10 +238,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if target_pat.is_empty(): # avoid some extra allocations
|
||||
continue
|
||||
|
||||
p = ref.as_pattern(pattern=flattened[target])
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
for ref in pat.refs[target]:
|
||||
p = ref.as_pattern(pattern=target_pat)
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
|
||||
pat.refs.clear()
|
||||
flattened[name] = pat
|
||||
@ -316,7 +315,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
names = set(self.keys())
|
||||
not_toplevel: set[str | None] = set()
|
||||
for name in names:
|
||||
not_toplevel |= set(sp.target for sp in self[name].refs)
|
||||
not_toplevel |= set(self[name].refs.keys())
|
||||
|
||||
toplevel = list(names - not_toplevel)
|
||||
return toplevel
|
||||
@ -352,9 +351,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
At each pattern in the tree, the following sequence is called:
|
||||
```
|
||||
current_pattern = visit_before(current_pattern, **vist_args)
|
||||
for sp in current_pattern.refs]
|
||||
self.dfs(sp.target, visit_before, visit_after,
|
||||
hierarchy + (sp.target,), updated_transform, memo)
|
||||
for target in current_pattern.refs:
|
||||
for ref in pattern.refs[target]:
|
||||
self.dfs(target, visit_before, visit_after,
|
||||
hierarchy + (sp.target,), updated_transform, memo)
|
||||
current_pattern = visit_after(current_pattern, **visit_args)
|
||||
```
|
||||
where `visit_args` are
|
||||
@ -398,32 +398,33 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
if visit_before is not None:
|
||||
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
||||
|
||||
for ref in pattern.refs:
|
||||
if transform is not False:
|
||||
sign = numpy.ones(2)
|
||||
if transform[3]:
|
||||
sign[1] = -1
|
||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
||||
mirror_x, angle = normalize_mirror(ref.mirrored)
|
||||
angle += ref.rotation
|
||||
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||
ref_transform[3] %= 2
|
||||
else:
|
||||
ref_transform = False
|
||||
|
||||
if ref.target is None:
|
||||
for target in pattern.refs:
|
||||
if target is None:
|
||||
continue
|
||||
if ref.target in hierarchy:
|
||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"')
|
||||
if target in hierarchy:
|
||||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
||||
|
||||
self.dfs(
|
||||
pattern=self[ref.target],
|
||||
visit_before=visit_before,
|
||||
visit_after=visit_after,
|
||||
hierarchy=hierarchy + (ref.target,),
|
||||
transform=ref_transform,
|
||||
memo=memo,
|
||||
)
|
||||
for ref in pattern.refs[target]:
|
||||
if transform is not False:
|
||||
sign = numpy.ones(2)
|
||||
if transform[3]:
|
||||
sign[1] = -1
|
||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
||||
mirror_x, angle = normalize_mirror(ref.mirrored)
|
||||
angle += ref.rotation
|
||||
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||
ref_transform[3] %= 2
|
||||
else:
|
||||
ref_transform = False
|
||||
|
||||
self.dfs(
|
||||
pattern=self[target],
|
||||
visit_before=visit_before,
|
||||
visit_after=visit_after,
|
||||
hierarchy=hierarchy + (target,),
|
||||
transform=ref_transform,
|
||||
memo=memo,
|
||||
)
|
||||
|
||||
if visit_after is not None:
|
||||
pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
|
||||
@ -508,9 +509,9 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
self
|
||||
"""
|
||||
for pattern in self.values():
|
||||
for ref in pattern.refs:
|
||||
if ref.target == old_target:
|
||||
ref.target = new_target
|
||||
if old_target in pattern.refs:
|
||||
pattern.refs[new_target].extend(pattern.refs[old_target])
|
||||
del pattern.refs[old_target]
|
||||
return self
|
||||
|
||||
def mkpat(self, name: str) -> tuple[str, 'Pattern']:
|
||||
@ -549,6 +550,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
from .pattern import map_targets
|
||||
duplicates = set(self.keys()) & set(other.keys())
|
||||
|
||||
if not duplicates:
|
||||
@ -572,8 +574,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# Update references in the newly-added cells
|
||||
for old_name in temp:
|
||||
new_name = rename_map.get(old_name, old_name)
|
||||
for ref in self[new_name].refs:
|
||||
ref.target = rename_map.get(cast(str, ref.target), ref.target)
|
||||
pat = self[new_name]
|
||||
pat.refs = map_targets(pat.refs, rename_map)
|
||||
|
||||
return rename_map
|
||||
|
||||
@ -644,11 +646,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# Using the label tuple from `.normalized_form()` as a key, check how many of each shape
|
||||
# are present and store the shape function for each one
|
||||
for pat in tuple(self.values()):
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if not any(isinstance(shape, t) for t in exclude_types):
|
||||
label, _values, func = shape.normalized_form(norm_value)
|
||||
shape_funcs[label] = func
|
||||
shape_counts[label] += 1
|
||||
for layer, sseq in pat.shapes.items():
|
||||
for shape in sseq:
|
||||
if not any(isinstance(shape, t) for t in exclude_types):
|
||||
base_label, _values, func = shape.normalized_form(norm_value)
|
||||
label = (*base_label, layer)
|
||||
shape_funcs[label] = func
|
||||
shape_counts[label] += 1
|
||||
|
||||
shape_pats = {}
|
||||
for label, count in shape_counts.items():
|
||||
@ -656,7 +660,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
continue
|
||||
|
||||
shape_func = shape_funcs[label]
|
||||
shape_pat = Pattern(shapes=[shape_func()])
|
||||
shape_pat = Pattern()
|
||||
shape_pat.shapes[label[-1]] += [shape_func()]
|
||||
shape_pats[label] = shape_pat
|
||||
|
||||
# ## Second pass ##
|
||||
@ -665,33 +670,36 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
# are to be replaced.
|
||||
# The `values` are `(offset, scale, rotation)`.
|
||||
|
||||
shape_table: MutableMapping[tuple, list] = defaultdict(list)
|
||||
for i, shape in enumerate(pat.shapes):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
shape_table: dict[tuple, list] = defaultdict(list)
|
||||
for layer, sseq in pat.shapes.items():
|
||||
for i, shape in enumerate(sseq):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
|
||||
label, values, _func = shape.normalized_form(norm_value)
|
||||
base_label, values, _func = shape.normalized_form(norm_value)
|
||||
label = (*base_label, layer)
|
||||
|
||||
if label not in shape_pats:
|
||||
continue
|
||||
if label not in shape_pats:
|
||||
continue
|
||||
|
||||
shape_table[label].append((i, values))
|
||||
shape_table[label].append((i, values))
|
||||
|
||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||
# and add `pat.refs` entries for each occurrence in pat. Also, note down that
|
||||
# we should delete the `pat.shapes` entries for which we made `Ref`s.
|
||||
shapes_to_remove = []
|
||||
for label in shape_table:
|
||||
layer = label[-1]
|
||||
target = label2name(label)
|
||||
for i, values in shape_table[label]:
|
||||
for ii, values in shape_table[label]:
|
||||
offset, scale, rotation, mirror_x = values
|
||||
pat.ref(target=target, offset=offset, scale=scale,
|
||||
rotation=rotation, mirrored=(mirror_x, False))
|
||||
shapes_to_remove.append(i)
|
||||
shapes_to_remove.append(ii)
|
||||
|
||||
# Remove any shapes for which we have created refs.
|
||||
for i in sorted(shapes_to_remove, reverse=True):
|
||||
del pat.shapes[i]
|
||||
# Remove any shapes for which we have created refs.
|
||||
for ii in sorted(shapes_to_remove, reverse=True):
|
||||
del pat.shapes[layer][ii]
|
||||
|
||||
for ll, pp in shape_pats.items():
|
||||
self[label2name(ll)] = pp
|
||||
@ -722,28 +730,30 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
#name_func = lambda _pat, _shape: self.get_name('_rep')
|
||||
|
||||
for pat in tuple(self.values()):
|
||||
new_shapes = []
|
||||
for shape in pat.shapes:
|
||||
if shape.repetition is None:
|
||||
new_shapes.append(shape)
|
||||
continue
|
||||
for layer in pat.shapes:
|
||||
new_shapes = []
|
||||
for shape in pat.shapes[layer]:
|
||||
if shape.repetition is None:
|
||||
new_shapes.append(shape)
|
||||
continue
|
||||
|
||||
name = name_func(pat, shape)
|
||||
self[name] = Pattern(shapes=[shape])
|
||||
pat.ref(name, repetition=shape.repetition)
|
||||
shape.repetition = None
|
||||
pat.shapes = new_shapes
|
||||
name = name_func(pat, shape)
|
||||
self[name] = Pattern(shapes={layer: [shape]})
|
||||
pat.ref(name, repetition=shape.repetition)
|
||||
shape.repetition = None
|
||||
pat.shapes[layer] = new_shapes
|
||||
|
||||
new_labels = []
|
||||
for label in pat.labels:
|
||||
if label.repetition is None:
|
||||
new_labels.append(label)
|
||||
continue
|
||||
name = name_func(pat, label)
|
||||
self[name] = Pattern(labels=[label])
|
||||
pat.ref(name, repetition=label.repetition)
|
||||
label.repetition = None
|
||||
pat.labels = new_labels
|
||||
for layer in pat.labels:
|
||||
new_labels = []
|
||||
for label in pat.labels[layer]:
|
||||
if label.repetition is None:
|
||||
new_labels.append(label)
|
||||
continue
|
||||
name = name_func(pat, label)
|
||||
self[name] = Pattern(labels={layer: [label]})
|
||||
pat.ref(name, repetition=label.repetition)
|
||||
label.repetition = None
|
||||
pat.labels[layer] = new_labels
|
||||
|
||||
return self
|
||||
|
||||
@ -782,8 +792,12 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
while empty := set(name for name, pat in self.items() if pat.is_empty()):
|
||||
for name in empty:
|
||||
del self[name]
|
||||
|
||||
for pat in self.values():
|
||||
pat.refs = [ref for ref in pat.refs if ref.target not in empty]
|
||||
for name in empty:
|
||||
# Second pass to skip looking at refs in empty patterns
|
||||
if name in pat.refs:
|
||||
del pat.refs[name]
|
||||
|
||||
trimmed |= empty
|
||||
if not repeat:
|
||||
@ -799,7 +813,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
del self[key]
|
||||
if delete_refs:
|
||||
for pat in self.values():
|
||||
pat.refs = [ref for ref in pat.refs if ref.target != key]
|
||||
if key in pat.refs:
|
||||
del pat.refs[key]
|
||||
return self
|
||||
|
||||
|
||||
@ -1007,9 +1022,9 @@ class LazyLibrary(ILibrary):
|
||||
"""
|
||||
self.precache()
|
||||
for pattern in self.cache.values():
|
||||
for ref in pattern.refs:
|
||||
if ref.target == old_target:
|
||||
ref.target = new_target
|
||||
if old_target in pattern.refs:
|
||||
pattern.refs[new_target].extend(pattern.refs[old_target])
|
||||
del pattern.refs[old_target]
|
||||
return self
|
||||
|
||||
def precache(self) -> Self:
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""
|
||||
Base object representing a lithography mask.
|
||||
"""
|
||||
|
||||
from typing import Callable, Sequence, cast, Mapping, Self, Any
|
||||
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar
|
||||
import copy
|
||||
import logging
|
||||
from itertools import chain
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy
|
||||
from numpy import inf
|
||||
@ -12,14 +13,17 @@ from numpy.typing import NDArray, ArrayLike
|
||||
# .visualize imports matplotlib and matplotlib.collections
|
||||
|
||||
from .ref import Ref
|
||||
from .shapes import Shape, Polygon, DEFAULT_POLY_NUM_VERTICES
|
||||
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
|
||||
from .label import Label
|
||||
from .utils import rotation_matrix_2d, annotations_t
|
||||
from .utils import rotation_matrix_2d, annotations_t, layer_t
|
||||
from .error import PatternError
|
||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
|
||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
||||
from .ports import Port, PortList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
"""
|
||||
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
||||
@ -31,15 +35,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
'_offset', '_annotations',
|
||||
)
|
||||
|
||||
shapes: list[Shape]
|
||||
""" List of all shapes in this Pattern.
|
||||
shapes: defaultdict[layer_t, list[Shape]]
|
||||
""" Stores of all shapes in this Pattern, indexed by layer.
|
||||
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
|
||||
"""
|
||||
|
||||
labels: list[Label]
|
||||
labels: defaultdict[layer_t, list[Label]]
|
||||
""" List of all labels in this Pattern. """
|
||||
|
||||
refs: list[Ref]
|
||||
refs: defaultdict[str | None, list[Ref]]
|
||||
""" List of all references to other patterns (`Ref`s) in this `Pattern`.
|
||||
Multiple objects in this list may reference the same Pattern object
|
||||
(i.e. multiple instances of the same object).
|
||||
@ -59,9 +63,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
shapes: Sequence[Shape] = (),
|
||||
labels: Sequence[Label] = (),
|
||||
refs: Sequence[Ref] = (),
|
||||
shapes: Mapping[layer_t, Sequence[Shape]] | None = None,
|
||||
labels: Mapping[layer_t, Sequence[Label]] | None = None,
|
||||
refs: Mapping[str | None, Sequence[Ref]] | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
ports: Mapping[str, 'Port'] | None = None
|
||||
) -> None:
|
||||
@ -76,20 +80,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
annotations: Initial annotations for the pattern
|
||||
ports: Any ports in the pattern
|
||||
"""
|
||||
if isinstance(shapes, list):
|
||||
self.shapes = shapes
|
||||
else:
|
||||
self.shapes = list(shapes)
|
||||
|
||||
if isinstance(labels, list):
|
||||
self.labels = labels
|
||||
else:
|
||||
self.labels = list(labels)
|
||||
|
||||
if isinstance(refs, list):
|
||||
self.refs = refs
|
||||
else:
|
||||
self.refs = list(refs)
|
||||
self.shapes = defaultdict(list)
|
||||
self.labels = defaultdict(list)
|
||||
self.refs = defaultdict(list)
|
||||
if shapes:
|
||||
for layer, sseq in shapes.items():
|
||||
self.shapes[layer].extend(sseq)
|
||||
if labels:
|
||||
for layer, lseq in labels.items():
|
||||
self.labels[layer].extend(lseq)
|
||||
if refs:
|
||||
for target, rseq in refs.items():
|
||||
self.refs[target].extend(rseq)
|
||||
|
||||
if ports is not None:
|
||||
self.ports = dict(copy.deepcopy(ports))
|
||||
@ -99,32 +101,42 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pattern: s{len(self.shapes)} r{len(self.refs)} l{len(self.labels)} ['
|
||||
nshapes = sum(len(seq) for seq in self.shapes.values())
|
||||
nrefs = sum(len(seq) for seq in self.refs.values())
|
||||
nlabels = sum(len(seq) for seq in self.labels.values())
|
||||
|
||||
s = f'<Pattern: s{nshapes} r{nrefs} l{nlabels} ['
|
||||
for name, port in self.ports.items():
|
||||
s += f'\n\t{name}: {port}'
|
||||
s += ']>'
|
||||
return s
|
||||
|
||||
def __copy__(self) -> 'Pattern':
|
||||
return Pattern(
|
||||
shapes=copy.deepcopy(self.shapes),
|
||||
labels=copy.deepcopy(self.labels),
|
||||
refs=[copy.copy(sp) for sp in self.refs],
|
||||
logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
|
||||
new = Pattern(
|
||||
annotations=copy.deepcopy(self.annotations),
|
||||
ports=copy.deepcopy(self.ports),
|
||||
)
|
||||
for target, rseq in self.refs.items():
|
||||
new.refs[target].extend(rseq)
|
||||
for layer, sseq in self.shapes.items():
|
||||
new.shapes[layer].extend(sseq)
|
||||
for layer, lseq in self.labels.items():
|
||||
new.labels[layer].extend(lseq)
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Pattern':
|
||||
memo = {} if memo is None else memo
|
||||
new = Pattern(
|
||||
shapes=copy.deepcopy(self.shapes, memo),
|
||||
labels=copy.deepcopy(self.labels, memo),
|
||||
refs=copy.deepcopy(self.refs, memo),
|
||||
annotations=copy.deepcopy(self.annotations, memo),
|
||||
ports=copy.deepcopy(self.ports),
|
||||
)
|
||||
return new
|
||||
|
||||
# def __deepcopy__(self, memo: dict | None = None) -> 'Pattern':
|
||||
# memo = {} if memo is None else memo
|
||||
# new = Pattern(
|
||||
# shapes=copy.deepcopy(self.shapes, memo),
|
||||
# labels=copy.deepcopy(self.labels, memo),
|
||||
# refs=copy.deepcopy(self.refs, memo),
|
||||
# annotations=copy.deepcopy(self.annotations, memo),
|
||||
# ports=copy.deepcopy(self.ports),
|
||||
# )
|
||||
# return new
|
||||
|
||||
def append(self, other_pattern: 'Pattern') -> Self:
|
||||
"""
|
||||
Appends all shapes, labels and refs from other_pattern to self's shapes,
|
||||
@ -136,9 +148,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.refs += other_pattern.refs
|
||||
self.shapes += other_pattern.shapes
|
||||
self.labels += other_pattern.labels
|
||||
for target, rseq in other_pattern.refs.items():
|
||||
self.refs[target].extend(rseq)
|
||||
for layer, sseq in other_pattern.shapes.items():
|
||||
self.shapes[layer].extend(sseq)
|
||||
for layer, lseq in other_pattern.labels.items():
|
||||
self.labels[layer].extend(lseq)
|
||||
|
||||
annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
|
||||
if annotation_conflicts:
|
||||
@ -154,9 +169,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
|
||||
def subset(
|
||||
self,
|
||||
shapes: Callable[[Shape], bool] | None = None,
|
||||
labels: Callable[[Label], bool] | None = None,
|
||||
refs: Callable[[Ref], bool] | None = None,
|
||||
shapes: Callable[[layer_t, Shape], bool] | None = None,
|
||||
labels: Callable[[layer_t, Label], bool] | None = None,
|
||||
refs: Callable[[str | None, Ref], bool] | None = None,
|
||||
annotations: Callable[[str, list[int | float | str]], bool] | None = None,
|
||||
ports: Callable[[str, Port], bool] | None = None,
|
||||
default_keep: bool = False
|
||||
@ -167,9 +182,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced.
|
||||
|
||||
Args:
|
||||
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
|
||||
labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
|
||||
refs: Given a ref, returns a boolean denoting if it is a member of the subset.
|
||||
shapes: Given a layer and shape, returns a boolean denoting whether the shape is a
|
||||
member of the subset.
|
||||
labels: Given a layer and label, returns a boolean denoting whether the label is a
|
||||
member of the subset.
|
||||
refs: Given a target and ref, returns a boolean denoting if it is a member of the subset.
|
||||
annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
|
||||
ports: Given a port, returns a boolean denoting if it is a member of the subset.
|
||||
default_keep: If `True`, keeps all elements of a given type if no function is supplied.
|
||||
@ -182,17 +199,20 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
pat = Pattern()
|
||||
|
||||
if shapes is not None:
|
||||
pat.shapes = [s for s in self.shapes if shapes(s)]
|
||||
for layer in self.shapes:
|
||||
pat.shapes[layer] = [ss for ss in self.shapes[layer] if shapes(layer, ss)]
|
||||
elif default_keep:
|
||||
pat.shapes = copy.copy(self.shapes)
|
||||
|
||||
if labels is not None:
|
||||
pat.labels = [s for s in self.labels if labels(s)]
|
||||
for layer in self.labels:
|
||||
pat.labels[layer] = [ll for ll in self.labels[layer] if labels(layer, ll)]
|
||||
elif default_keep:
|
||||
pat.labels = copy.copy(self.labels)
|
||||
|
||||
if refs is not None:
|
||||
pat.refs = [s for s in self.refs if refs(s)]
|
||||
for target in self.refs:
|
||||
pat.refs[target] = [rr for rr in self.refs[target] if refs(target, rr)]
|
||||
elif default_keep:
|
||||
pat.refs = copy.copy(self.refs)
|
||||
|
||||
@ -227,10 +247,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
old_shapes = self.shapes
|
||||
self.shapes = list(chain.from_iterable((
|
||||
shape.to_polygons(num_vertices, max_arclen)
|
||||
for shape in old_shapes)))
|
||||
for layer in self.shapes:
|
||||
self.shapes[layer] = list(chain.from_iterable(
|
||||
ss.to_polygons(num_vertices, max_arclen)
|
||||
for ss in self.shapes[layer]
|
||||
))
|
||||
return self
|
||||
|
||||
def manhattanize(
|
||||
@ -251,9 +272,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
"""
|
||||
|
||||
self.polygonize()
|
||||
old_shapes = self.shapes
|
||||
self.shapes = list(chain.from_iterable(
|
||||
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
|
||||
for layer in self.shapes:
|
||||
self.shapes[layer] = list(chain.from_iterable((
|
||||
ss.manhattanize(grid_x, grid_y)
|
||||
for ss in self.shapes[layer]
|
||||
)))
|
||||
return self
|
||||
|
||||
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]:
|
||||
@ -268,7 +291,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
is of the form `[[x0, y0], [x1, y1],...]`.
|
||||
"""
|
||||
pat = self.deepcopy().polygonize().flatten(library=library)
|
||||
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
|
||||
polys = [
|
||||
cast(Polygon, shape).vertices + cast(Polygon, shape).offset
|
||||
for shape in chain_elements(pat.shapes)
|
||||
]
|
||||
return polys
|
||||
|
||||
def referenced_patterns(self) -> set[str | None]:
|
||||
"""
|
||||
@ -277,7 +304,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
A set of all pattern names referenced by this pattern.
|
||||
"""
|
||||
return set(sp.target for sp in self.refs)
|
||||
return set(self.refs.keys())
|
||||
|
||||
def get_bounds(
|
||||
self,
|
||||
@ -301,23 +328,29 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
min_bounds = numpy.array((+inf, +inf))
|
||||
max_bounds = numpy.array((-inf, -inf))
|
||||
|
||||
for entry in chain(self.shapes, self.labels):
|
||||
bounds = entry.get_bounds()
|
||||
for entry in chain_elements(self.shapes, self.labels):
|
||||
bounds = cast(Bounded, entry).get_bounds()
|
||||
if bounds is None:
|
||||
continue
|
||||
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||
|
||||
if self.refs and (library is None):
|
||||
raise PatternError('Must provide a library to get_bounds() to resolve refs')
|
||||
if recurse and self.has_refs():
|
||||
if library is None:
|
||||
raise PatternError('Must provide a library to get_bounds() to resolve refs')
|
||||
|
||||
if recurse:
|
||||
for entry in self.refs:
|
||||
bounds = entry.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
for target, refs in self.refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||
if not refs:
|
||||
continue
|
||||
target_pat = library[target]
|
||||
for ref in refs:
|
||||
bounds = ref.get_bounds(target_pat, library=library)
|
||||
if bounds is None:
|
||||
continue
|
||||
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||
|
||||
if (max_bounds < min_bounds).any():
|
||||
return None
|
||||
@ -352,7 +385,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
|
||||
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
||||
cast(Positionable, entry).translate(offset)
|
||||
return self
|
||||
|
||||
@ -366,7 +399,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs):
|
||||
for entry in chain_elements(self.shapes, self.refs):
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
return self
|
||||
|
||||
@ -382,7 +415,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs):
|
||||
for entry in chain_elements(self.shapes, self.refs):
|
||||
cast(Positionable, entry).offset *= c
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
|
||||
@ -390,7 +423,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
if rep:
|
||||
rep.scale_by(c)
|
||||
|
||||
for label in self.labels:
|
||||
for label in chain_elements(self.labels):
|
||||
cast(Positionable, label).offset *= c
|
||||
|
||||
rep = cast(Repeatable, label).repetition
|
||||
@ -429,7 +462,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
|
||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||
old_offset = cast(Positionable, entry).offset
|
||||
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||
return self
|
||||
@ -444,7 +477,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs, self.ports.values()):
|
||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||
cast(Rotatable, entry).rotate(rotation)
|
||||
return self
|
||||
|
||||
@ -459,7 +492,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
|
||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||
cast(Positionable, entry).offset[across_axis - 1] *= -1
|
||||
return self
|
||||
|
||||
@ -475,7 +508,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain(self.shapes, self.refs, self.ports.values()):
|
||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||
cast(Mirrorable, entry).mirror(across_axis)
|
||||
return self
|
||||
|
||||
@ -521,68 +554,95 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
Returns:
|
||||
True if the pattern is contains no shapes, labels, or refs.
|
||||
"""
|
||||
return (len(self.refs) == 0
|
||||
and len(self.shapes) == 0
|
||||
and len(self.labels) == 0)
|
||||
return not (self.has_refs() or self.has_shapes() or self.has_labels())
|
||||
|
||||
def ref(self, *args: Any, **kwargs: Any) -> Self:
|
||||
def has_refs(self) -> bool:
|
||||
return any(True for _ in chain.from_iterable(self.refs.values()))
|
||||
|
||||
def has_shapes(self) -> bool:
|
||||
return any(True for _ in chain.from_iterable(self.shapes.values()))
|
||||
|
||||
def has_labels(self) -> bool:
|
||||
return any(True for _ in chain.from_iterable(self.labels.values()))
|
||||
|
||||
def ref(self, target: str | None, *args: Any, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Convenience function which constructs a `Ref` object and adds it
|
||||
to this pattern.
|
||||
|
||||
Args:
|
||||
target: Target for the ref
|
||||
*args: Passed to `Ref()`
|
||||
**kwargs: Passed to `Ref()`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.refs.append(Ref(*args, **kwargs))
|
||||
self.refs[target].append(Ref(*args, **kwargs))
|
||||
return self
|
||||
|
||||
def polygon(self, *args: Any, **kwargs: Any) -> Self:
|
||||
def polygon(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Convenience function which constructs a `Polygon` object and adds it
|
||||
to this pattern.
|
||||
|
||||
Args:
|
||||
layer: Layer for the polygon
|
||||
*args: Passed to `Polygon()`
|
||||
**kwargs: Passed to `Polygon()`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.shapes.append(Polygon(*args, **kwargs))
|
||||
self.shapes[layer].append(Polygon(*args, **kwargs))
|
||||
return self
|
||||
|
||||
def rect(self, *args: Any, **kwargs: Any) -> Self:
|
||||
def rect(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Convenience function which calls `Polygon.rect` to construct a
|
||||
rectangle and adds it to this pattern.
|
||||
|
||||
Args:
|
||||
layer: Layer for the rectangle
|
||||
*args: Passed to `Polygon.rect()`
|
||||
**kwargs: Passed to `Polygon.rect()`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.shapes.append(Polygon.rect(*args, **kwargs))
|
||||
self.shapes[layer].append(Polygon.rect(*args, **kwargs))
|
||||
return self
|
||||
|
||||
def label(self, *args: Any, **kwargs: Any) -> Self:
|
||||
def path(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Convenience function which constructs a `Path` object and adds it
|
||||
to this pattern.
|
||||
|
||||
Args:
|
||||
layer: Layer for the path
|
||||
*args: Passed to `Path()`
|
||||
**kwargs: Passed to `Path()`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.shapes[layer].append(Path(*args, **kwargs))
|
||||
return self
|
||||
|
||||
def label(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Convenience function which constructs a `Label` object
|
||||
and adds it to this pattern.
|
||||
|
||||
Args:
|
||||
layer: Layer for the label
|
||||
*args: Passed to `Label()`
|
||||
**kwargs: Passed to `Label()`
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
self.labels.append(Label(*args, **kwargs))
|
||||
self.labels[layer].append(Label(*args, **kwargs))
|
||||
return self
|
||||
|
||||
def flatten(
|
||||
@ -610,24 +670,26 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
pat = library[name].deepcopy()
|
||||
flattened[name] = None
|
||||
|
||||
for ref in pat.refs:
|
||||
target = ref.target
|
||||
for target, refs in pat.refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
if not refs:
|
||||
continue
|
||||
|
||||
if target not in flattened:
|
||||
flatten_single(target)
|
||||
|
||||
target_pat = flattened[target]
|
||||
|
||||
if target_pat is None:
|
||||
raise PatternError(f'Circular reference in {name} to {target}')
|
||||
if target_pat.is_empty(): # avoid some extra allocations
|
||||
continue
|
||||
|
||||
p = ref.as_pattern(pattern=flattened[target])
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
for ref in refs:
|
||||
p = ref.as_pattern(pattern=target_pat)
|
||||
if not flatten_ports:
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
|
||||
pat.refs.clear()
|
||||
flattened[name] = pat
|
||||
@ -661,7 +723,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
from matplotlib import pyplot # type: ignore
|
||||
import matplotlib.collections # type: ignore
|
||||
|
||||
if self.refs and library is None:
|
||||
if self.has_refs() and library is None:
|
||||
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||
|
||||
offset = numpy.array(offset, dtype=float)
|
||||
@ -675,7 +737,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
axes = figure.gca()
|
||||
|
||||
polygons = []
|
||||
for shape in self.shapes:
|
||||
for shape in chain.from_iterable(self.shapes.values()):
|
||||
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
|
||||
|
||||
mpl_poly_collection = matplotlib.collections.PolyCollection(
|
||||
@ -686,16 +748,52 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
axes.add_collection(mpl_poly_collection)
|
||||
pyplot.axis('equal')
|
||||
|
||||
for ref in self.refs:
|
||||
ref.as_pattern(library=library).visualize(
|
||||
library=library,
|
||||
offset=offset,
|
||||
overdraw=True,
|
||||
line_color=line_color,
|
||||
fill_color=fill_color,
|
||||
)
|
||||
for target, refs in self.refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
if not refs:
|
||||
continue
|
||||
assert library is not None
|
||||
target_pat = library[target]
|
||||
for ref in refs:
|
||||
ref.as_pattern(target_pat).visualize(
|
||||
library=library,
|
||||
offset=offset,
|
||||
overdraw=True,
|
||||
line_color=line_color,
|
||||
fill_color=fill_color,
|
||||
)
|
||||
|
||||
if not overdraw:
|
||||
pyplot.xlabel('x')
|
||||
pyplot.ylabel('y')
|
||||
pyplot.show()
|
||||
|
||||
|
||||
TT = TypeVar('TT')
|
||||
|
||||
|
||||
def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[TT]:
|
||||
return chain(*(chain.from_iterable(aa.values()) for aa in args))
|
||||
|
||||
|
||||
def map_layers(
|
||||
elements: Mapping[layer_t, Sequence[TT]],
|
||||
layer_map: Mapping[layer_t, layer_t],
|
||||
) -> defaultdict[layer_t, list[TT]]:
|
||||
new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
|
||||
for old_layer, seq in elements.items():
|
||||
new_layer = layer_map.get(old_layer, old_layer)
|
||||
new_elements[new_layer].extend(seq)
|
||||
return new_elements
|
||||
|
||||
|
||||
def map_targets(
|
||||
refs: Mapping[str | None, Sequence[Ref]],
|
||||
target_map: Mapping[str | None, str | None] | Mapping[str, str | None],
|
||||
) -> defaultdict[str | None, list[Ref]]:
|
||||
new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list)
|
||||
for old_target, seq in refs.items():
|
||||
new_target = target_map.get(old_target, old_target) # type: ignore # OK to .get() wrong type
|
||||
new_refs[new_target].extend(seq)
|
||||
return new_refs
|
||||
|
@ -33,20 +33,16 @@ class Ref(
|
||||
offset, rotation, scaling, and associated methods.
|
||||
"""
|
||||
__slots__ = (
|
||||
'_target', '_mirrored',
|
||||
'_mirrored',
|
||||
# inherited
|
||||
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_target: str | None
|
||||
""" The name of the `Pattern` being instanced """
|
||||
|
||||
_mirrored: NDArray[numpy.bool_]
|
||||
""" Whether to mirror the instance across the x and/or y axes. """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target: str | None,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
@ -57,14 +53,12 @@ class Ref(
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
target: Name of the Pattern to reference.
|
||||
offset: (x, y) offset applied to the referenced pattern. Not affected by rotation etc.
|
||||
rotation: Rotation (radians, counterclockwise) relative to the referenced pattern's (0, 0).
|
||||
mirrored: Whether to mirror the referenced pattern across its x and y axes.
|
||||
scale: Scaling factor applied to the pattern's geometry.
|
||||
repetition: `Repetition` object, default `None`
|
||||
"""
|
||||
self.target = target
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.scale = scale
|
||||
@ -76,7 +70,6 @@ class Ref(
|
||||
|
||||
def __copy__(self) -> 'Ref':
|
||||
new = Ref(
|
||||
target=self.target,
|
||||
offset=self.offset.copy(),
|
||||
rotation=self.rotation,
|
||||
scale=self.scale,
|
||||
@ -93,17 +86,6 @@ class Ref(
|
||||
new.annotations = copy.deepcopy(self.annotations, memo)
|
||||
return new
|
||||
|
||||
# target property
|
||||
@property
|
||||
def target(self) -> str | None:
|
||||
return self._target
|
||||
|
||||
@target.setter
|
||||
def target(self, val: str | None) -> None:
|
||||
if val is not None and not isinstance(val, str):
|
||||
raise PatternError(f'Provided target {val} is not a str or None!')
|
||||
self._target = val
|
||||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> Any: # TODO mypy#3004 NDArray[numpy.bool_]:
|
||||
@ -117,27 +99,16 @@ class Ref(
|
||||
|
||||
def as_pattern(
|
||||
self,
|
||||
*,
|
||||
pattern: 'Pattern | None' = None,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
pattern: 'Pattern',
|
||||
) -> 'Pattern':
|
||||
"""
|
||||
Args:
|
||||
pattern: Pattern object to transform
|
||||
library: A str->Pattern mapping, used instead of `pattern`. Must contain
|
||||
`self.target`.
|
||||
|
||||
Returns:
|
||||
A copy of the referenced Pattern which has been scaled, rotated, etc.
|
||||
according to this `Ref`'s properties.
|
||||
"""
|
||||
if pattern is None:
|
||||
if library is None:
|
||||
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||
|
||||
assert self.target is not None
|
||||
pattern = library[self.target]
|
||||
|
||||
pattern = pattern.deepcopy()
|
||||
|
||||
if self.scale != 1:
|
||||
@ -175,8 +146,8 @@ class Ref(
|
||||
|
||||
def get_bounds(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
pattern: 'Pattern | None' = None,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
@ -190,20 +161,29 @@ class Ref(
|
||||
Returns:
|
||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||
"""
|
||||
if pattern is None and library is None:
|
||||
raise PatternError('as_pattern() must be given a pattern or library.')
|
||||
if pattern is None and self.target is None:
|
||||
return None
|
||||
if library is not None and self.target not in library:
|
||||
raise PatternError(f'get_bounds() called on dangling reference to "{self.target}"')
|
||||
if pattern is not None and pattern.is_empty():
|
||||
if pattern.is_empty():
|
||||
# no need to run as_pattern()
|
||||
return None
|
||||
return self.as_pattern(pattern=pattern, library=library).get_bounds(library)
|
||||
return self.as_pattern(pattern=pattern).get_bounds(library) # TODO can just take pattern's bounds and then transform those!
|
||||
|
||||
def get_bounds_nonempty(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
*,
|
||||
library: Mapping[str, 'Pattern'] | None = None,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
|
||||
Asserts that the entity is non-empty (i.e., `get_bounds()` does not return None).
|
||||
|
||||
This is handy for destructuring like `xy_min, xy_max = entity.get_bounds_nonempty()`
|
||||
"""
|
||||
bounds = self.get_bounds(pattern, library=library)
|
||||
assert bounds is not None
|
||||
return bounds
|
||||
|
||||
def __repr__(self) -> str:
|
||||
name = f'"{self.target}"' if self.target is not None else None
|
||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||
return f'<Ref {name} at {self.offset}{rotation}{scale}{mirrored}>'
|
||||
return f'<Ref {self.offset}{rotation}{scale}{mirrored}>'
|
||||
|
@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, layer_t, annotations_t
|
||||
from ..utils import is_scalar, annotations_t
|
||||
|
||||
|
||||
class Arc(Shape):
|
||||
@ -24,7 +24,7 @@ class Arc(Shape):
|
||||
__slots__ = (
|
||||
'_radii', '_angles', '_width', '_rotation',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
@ -156,7 +156,6 @@ class Arc(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -172,7 +171,6 @@ class Arc(Shape):
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radii = radii
|
||||
self.angles = angles
|
||||
@ -181,7 +179,6 @@ class Arc(Shape):
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
||||
@ -241,7 +238,7 @@ class Arc(Shape):
|
||||
ys = numpy.hstack((ys1, ys2))
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
@ -352,13 +349,12 @@ class Arc(Shape):
|
||||
rotation %= 2 * pi
|
||||
width = self.width
|
||||
|
||||
return ((type(self), radii, angles, width / norm_value, self.layer),
|
||||
return ((type(self), radii, angles, width / norm_value),
|
||||
(self.offset, scale / norm_value, rotation, False),
|
||||
lambda: Arc(
|
||||
radii=radii * norm_value,
|
||||
angles=angles,
|
||||
width=width * norm_value,
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def get_cap_edges(self) -> NDArray[numpy.float64]:
|
||||
@ -415,4 +411,4 @@ class Arc(Shape):
|
||||
def __repr__(self) -> str:
|
||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Arc l{self.layer} o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
|
@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, layer_t, annotations_t
|
||||
from ..utils import is_scalar, annotations_t
|
||||
|
||||
|
||||
class Circle(Shape):
|
||||
@ -17,7 +17,7 @@ class Circle(Shape):
|
||||
__slots__ = (
|
||||
'_radius',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radius: float
|
||||
@ -44,7 +44,6 @@ class Circle(Shape):
|
||||
radius: float,
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -55,13 +54,11 @@ class Circle(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radius = radius
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
||||
memo = {} if memo is None else memo
|
||||
@ -90,7 +87,7 @@ class Circle(Shape):
|
||||
ys = numpy.sin(thetas) * self.radius
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
return [Polygon(xys, offset=self.offset, layer=self.layer)]
|
||||
return [Polygon(xys, offset=self.offset)]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
return numpy.vstack((self.offset - self.radius,
|
||||
@ -110,9 +107,9 @@ class Circle(Shape):
|
||||
def normalized_form(self, norm_value) -> normalized_shape_tuple:
|
||||
rotation = 0.0
|
||||
magnitude = self.radius / norm_value
|
||||
return ((type(self), self.layer),
|
||||
return ((type(self),),
|
||||
(self.offset, magnitude, rotation, False),
|
||||
lambda: Circle(radius=norm_value, layer=self.layer))
|
||||
lambda: Circle(radius=norm_value))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Circle l{self.layer} o{self.offset} r{self.radius:g}>'
|
||||
return f'<Circle o{self.offset} r{self.radius:g}>'
|
||||
|
@ -9,7 +9,7 @@ from numpy.typing import ArrayLike, NDArray
|
||||
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t, annotations_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t
|
||||
|
||||
|
||||
class Ellipse(Shape):
|
||||
@ -20,7 +20,7 @@ class Ellipse(Shape):
|
||||
__slots__ = (
|
||||
'_radii', '_rotation',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_radii: NDArray[numpy.float64]
|
||||
@ -91,7 +91,6 @@ class Ellipse(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -104,14 +103,12 @@ class Ellipse(Shape):
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.radii = radii
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Ellipse':
|
||||
@ -152,7 +149,7 @@ class Ellipse(Shape):
|
||||
ys = r1 * sin_th
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
poly = Polygon(xys, layer=self.layer, offset=self.offset, rotation=self.rotation)
|
||||
poly = Polygon(xys, offset=self.offset, rotation=self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
@ -183,10 +180,10 @@ class Ellipse(Shape):
|
||||
radii = self.radii[::-1] / self.radius_y
|
||||
scale = self.radius_y
|
||||
angle = (self.rotation + pi / 2) % pi
|
||||
return ((type(self), radii, self.layer),
|
||||
return ((type(self), radii),
|
||||
(self.offset, scale / norm_value, angle, False),
|
||||
lambda: Ellipse(radii=radii * norm_value, layer=self.layer))
|
||||
lambda: Ellipse(radii=radii * norm_value))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
return f'<Ellipse l{self.layer} o{self.offset} r{self.radii}{rotation}>'
|
||||
rotation = f' r{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Ellipse o{self.offset} r{self.radii}{rotation}>'
|
||||
|
@ -9,7 +9,7 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple, Polygon, Circle
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ class Path(Shape):
|
||||
__slots__ = (
|
||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
_vertices: NDArray[numpy.float64]
|
||||
_width: float
|
||||
@ -154,7 +154,6 @@ class Path(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -169,7 +168,6 @@ class Path(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
self._width = width
|
||||
self._cap = cap
|
||||
self._cap_extensions = cap_extensions
|
||||
@ -178,7 +176,6 @@ class Path(Shape):
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
self.width = width
|
||||
self.cap = cap
|
||||
self.cap_extensions = cap_extensions
|
||||
@ -204,7 +201,6 @@ class Path(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
) -> 'Path':
|
||||
"""
|
||||
Build a path by specifying the turn angles and travel distances
|
||||
@ -224,7 +220,6 @@ class Path(Shape):
|
||||
mirrored: Whether to mirror across the x or y axes. For example,
|
||||
`mirrored=(True, False)` results in a reflection across the x-axis,
|
||||
multiplying the path's y-coordinates by -1. Default `(False, False)`
|
||||
layer: Layer, default `0`
|
||||
|
||||
Returns:
|
||||
The resulting Path object
|
||||
@ -238,8 +233,7 @@ class Path(Shape):
|
||||
verts.append(verts[-1] + direction * distance)
|
||||
|
||||
return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions,
|
||||
offset=offset, rotation=rotation, mirrored=mirrored,
|
||||
layer=layer)
|
||||
offset=offset, rotation=rotation, mirrored=mirrored)
|
||||
|
||||
def to_polygons(
|
||||
self,
|
||||
@ -254,7 +248,7 @@ class Path(Shape):
|
||||
|
||||
if self.width == 0:
|
||||
verts = numpy.vstack((v, v[::-1]))
|
||||
return [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
|
||||
return [Polygon(offset=self.offset, vertices=verts)]
|
||||
|
||||
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
|
||||
|
||||
@ -305,12 +299,12 @@ class Path(Shape):
|
||||
o1.append(v[-1] - perp[-1])
|
||||
verts = numpy.vstack((o0, o1[::-1]))
|
||||
|
||||
polys = [Polygon(offset=self.offset, vertices=verts, layer=self.layer)]
|
||||
polys = [Polygon(offset=self.offset, vertices=verts)]
|
||||
|
||||
if self.cap == PathCap.Circle:
|
||||
#for vert in v: # not sure if every vertex, or just ends?
|
||||
for vert in [v[0], v[-1]]:
|
||||
circ = Circle(offset=vert, radius=self.width / 2, layer=self.layer)
|
||||
circ = Circle(offset=vert, radius=self.width / 2)
|
||||
polys += circ.to_polygons(num_vertices=num_vertices, max_arclen=max_arclen)
|
||||
|
||||
return polys
|
||||
@ -370,13 +364,12 @@ class Path(Shape):
|
||||
|
||||
width0 = self.width / norm_value
|
||||
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer),
|
||||
return ((type(self), reordered_vertices.data.tobytes(), width0, self.cap),
|
||||
(offset, scale / norm_value, rotation, False),
|
||||
lambda: Path(
|
||||
reordered_vertices * norm_value,
|
||||
width=self.width * norm_value,
|
||||
cap=self.cap,
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def clean_vertices(self) -> 'Path':
|
||||
@ -422,4 +415,4 @@ class Path(Shape):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.offset + self.vertices.mean(axis=0)
|
||||
return f'<Path l{self.layer} centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||
|
@ -8,7 +8,7 @@ from numpy.typing import NDArray, ArrayLike
|
||||
from . import Shape, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, layer_t
|
||||
from ..utils import is_scalar, rotation_matrix_2d
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ class Polygon(Shape):
|
||||
__slots__ = (
|
||||
'_vertices',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_vertices: NDArray[numpy.float64]
|
||||
@ -82,7 +82,6 @@ class Polygon(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
mirrored: Sequence[bool] = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -94,13 +93,11 @@ class Polygon(Shape):
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._layer = layer
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.layer = layer
|
||||
self.rotate(rotation)
|
||||
[self.mirror(a) for a, do in enumerate(mirrored) if do]
|
||||
|
||||
@ -118,7 +115,6 @@ class Polygon(Shape):
|
||||
*,
|
||||
rotation: float = 0.0,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -128,7 +124,6 @@ class Polygon(Shape):
|
||||
side_length: Length of one side
|
||||
rotation: Rotation counterclockwise, in radians
|
||||
offset: Offset, default `(0, 0)`
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -139,7 +134,7 @@ class Polygon(Shape):
|
||||
[+1, +1],
|
||||
[+1, -1]], dtype=float)
|
||||
vertices = 0.5 * side_length * norm_square
|
||||
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=offset, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
@ -150,7 +145,6 @@ class Polygon(Shape):
|
||||
*,
|
||||
rotation: float = 0,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -161,7 +155,6 @@ class Polygon(Shape):
|
||||
ly: Length along y (before rotation)
|
||||
rotation: Rotation counterclockwise, in radians
|
||||
offset: Offset, default `(0, 0)`
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -171,7 +164,7 @@ class Polygon(Shape):
|
||||
[-lx, +ly],
|
||||
[+lx, +ly],
|
||||
[+lx, -ly]], dtype=float)
|
||||
poly = Polygon(vertices, offset=offset, layer=layer, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=offset, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
@ -186,7 +179,6 @@ class Polygon(Shape):
|
||||
yctr: float | None = None,
|
||||
ymax: float | None = None,
|
||||
ly: float | None = None,
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -204,7 +196,6 @@ class Polygon(Shape):
|
||||
yctr: Center y coordinate
|
||||
ymax: Maximum y coordinate
|
||||
ly: Length along y direction
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -270,7 +261,7 @@ class Polygon(Shape):
|
||||
else:
|
||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||
|
||||
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), layer=layer, repetition=repetition)
|
||||
poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), repetition=repetition)
|
||||
return poly
|
||||
|
||||
@staticmethod
|
||||
@ -281,7 +272,6 @@ class Polygon(Shape):
|
||||
regular: bool = True,
|
||||
center: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
) -> 'Polygon':
|
||||
"""
|
||||
@ -300,7 +290,6 @@ class Polygon(Shape):
|
||||
rotation: Rotation counterclockwise, in radians.
|
||||
`0` results in four axis-aligned sides (the long sides of the
|
||||
irregular octagon).
|
||||
layer: Layer, default `0`
|
||||
repetition: `Repetition` object, default `None`
|
||||
|
||||
Returns:
|
||||
@ -327,7 +316,7 @@ class Polygon(Shape):
|
||||
side_length = 2 * inner_radius / s
|
||||
|
||||
vertices = 0.5 * side_length * norm_oct
|
||||
poly = Polygon(vertices, offset=center, layer=layer, repetition=repetition)
|
||||
poly = Polygon(vertices, offset=center, repetition=repetition)
|
||||
poly.rotate(rotation)
|
||||
return poly
|
||||
|
||||
@ -378,9 +367,9 @@ class Polygon(Shape):
|
||||
|
||||
# TODO: normalize mirroring?
|
||||
|
||||
return ((type(self), reordered_vertices.data.tobytes(), self.layer),
|
||||
return ((type(self), reordered_vertices.data.tobytes()),
|
||||
(offset, scale / norm_value, rotation, False),
|
||||
lambda: Polygon(reordered_vertices * norm_value, layer=self.layer))
|
||||
lambda: Polygon(reordered_vertices * norm_value))
|
||||
|
||||
def clean_vertices(self) -> 'Polygon':
|
||||
"""
|
||||
@ -414,4 +403,4 @@ class Polygon(Shape):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.offset + self.vertices.mean(axis=0)
|
||||
return f'<Polygon l{self.layer} centroid {centroid} v{len(self.vertices)}>'
|
||||
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
||||
|
@ -5,9 +5,8 @@ import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..traits import (
|
||||
Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PositionableImpl, LayerableImpl,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
Rotatable, Mirrorable, Copyable, Scalable, Bounded,
|
||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -26,7 +25,7 @@ normalized_shape_tuple = tuple[
|
||||
DEFAULT_POLY_NUM_VERTICES = 24
|
||||
|
||||
|
||||
class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable, Bounded,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Class specifying functions common to all shapes.
|
||||
@ -194,10 +193,7 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc
|
||||
vertex_lists.append(vlist)
|
||||
polygon_contours.append(numpy.vstack(vertex_lists))
|
||||
|
||||
manhattan_polygons = [
|
||||
Polygon(vertices=contour, layer=self.layer)
|
||||
for contour in polygon_contours
|
||||
]
|
||||
manhattan_polygons = [Polygon(vertices=contour) for contour in polygon_contours]
|
||||
|
||||
return manhattan_polygons
|
||||
|
||||
@ -292,9 +288,6 @@ class Shape(PositionableImpl, LayerableImpl, Rotatable, Mirrorable, Copyable, Sc
|
||||
vertices = numpy.hstack((grx[snapped_contour[:, None, 0] + offset_i[0]],
|
||||
gry[snapped_contour[:, None, 1] + offset_i[1]]))
|
||||
|
||||
manhattan_polygons.append(Polygon(
|
||||
vertices=vertices,
|
||||
layer=self.layer,
|
||||
))
|
||||
manhattan_polygons.append(Polygon(vertices=vertices))
|
||||
|
||||
return manhattan_polygons
|
||||
|
@ -9,7 +9,7 @@ from . import Shape, Polygon, normalized_shape_tuple
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, normalize_mirror, layer_t
|
||||
from ..utils import is_scalar, get_bit, normalize_mirror
|
||||
from ..utils import annotations_t
|
||||
|
||||
# Loaded on use:
|
||||
@ -25,7 +25,7 @@ class Text(RotatableImpl, Shape):
|
||||
__slots__ = (
|
||||
'_string', '_height', '_mirrored', 'font_path',
|
||||
# Inherited
|
||||
'_offset', '_layer', '_repetition', '_annotations', '_rotation',
|
||||
'_offset', '_repetition', '_annotations', '_rotation',
|
||||
)
|
||||
|
||||
_string: str
|
||||
@ -73,7 +73,6 @@ class Text(RotatableImpl, Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
mirrored: ArrayLike = (False, False),
|
||||
layer: layer_t = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
@ -82,7 +81,6 @@ class Text(RotatableImpl, Shape):
|
||||
assert isinstance(offset, numpy.ndarray)
|
||||
assert isinstance(mirrored, numpy.ndarray)
|
||||
self._offset = offset
|
||||
self._layer = layer
|
||||
self._string = string
|
||||
self._height = height
|
||||
self._rotation = rotation
|
||||
@ -91,7 +89,6 @@ class Text(RotatableImpl, Shape):
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.offset = offset
|
||||
self.layer = layer
|
||||
self.string = string
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
@ -120,7 +117,7 @@ class Text(RotatableImpl, Shape):
|
||||
|
||||
# Move these polygons to the right of the previous letter
|
||||
for xys in raw_polys:
|
||||
poly = Polygon(xys, layer=self.layer)
|
||||
poly = Polygon(xys)
|
||||
poly.mirror2d(self.mirrored)
|
||||
poly.scale_by(self.height)
|
||||
poly.offset = self.offset + [total_advance, 0]
|
||||
@ -144,7 +141,7 @@ class Text(RotatableImpl, Shape):
|
||||
mirror_x, rotation = normalize_mirror(self.mirrored)
|
||||
rotation += self.rotation
|
||||
rotation %= 2 * pi
|
||||
return ((type(self), self.string, self.font_path, self.layer),
|
||||
return ((type(self), self.string, self.font_path),
|
||||
(self.offset, self.height / norm_value, rotation, mirror_x),
|
||||
lambda: Text(
|
||||
string=self.string,
|
||||
@ -152,7 +149,6 @@ class Text(RotatableImpl, Shape):
|
||||
font_path=self.font_path,
|
||||
rotation=rotation,
|
||||
mirrored=(mirror_x, False),
|
||||
layer=self.layer,
|
||||
))
|
||||
|
||||
def get_bounds(self) -> NDArray[numpy.float64]:
|
||||
@ -256,6 +252,6 @@ def get_char_as_polygons(
|
||||
return polygons, advance
|
||||
|
||||
def __repr__(self) -> str:
|
||||
rotation = f' r°{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||
return f'<TextShape "{self.string}" l{self.layer} o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
return f'<TextShape "{self.string}" o{self.offset} h{self.height:g}{rotation}{mirrored}>'
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""
|
||||
Traits (mixins) and default implementations
|
||||
"""
|
||||
from .positionable import Positionable, PositionableImpl
|
||||
from .positionable import Positionable, PositionableImpl, Bounded
|
||||
from .layerable import Layerable, LayerableImpl
|
||||
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
|
||||
from .repeatable import Repeatable, RepeatableImpl
|
||||
|
@ -60,6 +60,8 @@ class Positionable(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Bounded(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_bounds(self) -> NDArray[numpy.float64] | None:
|
||||
"""
|
||||
|
@ -8,7 +8,6 @@ from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..error import MasqueError
|
||||
from ..pattern import Pattern
|
||||
from ..ref import Ref
|
||||
|
||||
|
||||
def maxrects_bssf(
|
||||
@ -160,8 +159,8 @@ def pack_patterns(
|
||||
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
|
||||
|
||||
pat = Pattern()
|
||||
pat.refs = [Ref(pp, offset=oo + loc)
|
||||
for pp, oo, loc in zip(patterns, offsets, locations)]
|
||||
for pp, oo, loc in zip(patterns, offsets, locations):
|
||||
pat.ref(pp, offset=oo + loc)
|
||||
|
||||
rejects = [patterns[ii] for ii in reject_inds]
|
||||
return pat, rejects
|
||||
|
@ -8,11 +8,11 @@ to write equivalent functions for your own format or alternate storage methods.
|
||||
"""
|
||||
from typing import Sequence, Mapping
|
||||
import logging
|
||||
from itertools import chain
|
||||
|
||||
import numpy
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..label import Label
|
||||
from ..utils import layer_t
|
||||
from ..ports import Port
|
||||
from ..error import PatternError
|
||||
@ -44,9 +44,7 @@ def ports_to_data(pattern: Pattern, layer: layer_t) -> Pattern:
|
||||
angle_deg = numpy.inf
|
||||
else:
|
||||
angle_deg = numpy.rad2deg(port.rotation)
|
||||
pattern.labels += [
|
||||
Label(string=f'{name}:{port.ptype} {angle_deg:g}', layer=layer, offset=port.offset)
|
||||
]
|
||||
pattern.label(layer=layer, string=f'{name}:{port.ptype} {angle_deg:g}', offset=port.offset)
|
||||
return pattern
|
||||
|
||||
|
||||
@ -97,7 +95,7 @@ def data_to_ports(
|
||||
|
||||
# Load ports for all subpatterns, and use any we find
|
||||
found_ports = False
|
||||
for target in set(rr.target for rr in pattern.refs):
|
||||
for target in pattern.refs:
|
||||
if target is None:
|
||||
continue
|
||||
pp = data_to_ports(
|
||||
@ -113,17 +111,20 @@ def data_to_ports(
|
||||
if not found_ports:
|
||||
return pattern
|
||||
|
||||
for ref in pattern.refs:
|
||||
if ref.target is None:
|
||||
for target, refs in pattern.refs.items():
|
||||
if target is None:
|
||||
continue
|
||||
aa = library.abstract(ref.target)
|
||||
if not aa.ports:
|
||||
if not refs:
|
||||
continue
|
||||
|
||||
aa.apply_ref_transform(ref)
|
||||
for ref in refs:
|
||||
aa = library.abstract(target)
|
||||
if not aa.ports:
|
||||
break
|
||||
|
||||
pattern.check_ports(other_names=aa.ports.keys())
|
||||
pattern.ports.update(aa.ports)
|
||||
aa.apply_ref_transform(ref)
|
||||
pattern.check_ports(other_names=aa.ports.keys())
|
||||
pattern.ports.update(aa.ports)
|
||||
return pattern
|
||||
|
||||
|
||||
@ -149,13 +150,13 @@ def data_to_ports_flat(
|
||||
Returns:
|
||||
The updated `pattern`. Port labels are not removed.
|
||||
"""
|
||||
labels = [ll for ll in pattern.labels if ll.layer in layers]
|
||||
labels = list(chain.from_iterable((pattern.labels[layer] for layer in layers)))
|
||||
if not labels:
|
||||
return pattern
|
||||
|
||||
pstr = cell_name if cell_name is not None else repr(pattern)
|
||||
if pattern.ports:
|
||||
raise PatternError('Pattern "{pstr}" has pre-existing ports!')
|
||||
raise PatternError(f'Pattern "{pstr}" has pre-existing ports!')
|
||||
|
||||
local_ports = {}
|
||||
for label in labels:
|
||||
|
Loading…
Reference in New Issue
Block a user