from .port import Port
from .transform import AffineTransform, rotate2d, translate2d, reflect2d, skew2d, scale2d
from .typing import Union, Float4, Float2
from typing import List, Dict, Optional, Tuple
import numpy as np
[docs]class Geometry:
def __init__(self, geoms: List[np.ndarray], port: Dict[str, Port], refs: List["Geometry"],
tangents: List[np.ndarray] = None):
self.geoms = geoms
self.port = port
self.refs = refs
self.curve = None # reserved for paths.
self.tangents = [] if tangents is None else tangents
@property
def points(self) -> np.ndarray:
return np.hstack(self.geoms) if len(self.geoms) > 0 else np.zeros((2, 0))
@property
def num_geoms(self) -> int:
return len(self.geoms)
@property
def num_points(self) -> int:
return len(self.points)
@property
def bounds(self) -> Float4:
"""Bounds of the geom.
Returns:
Tuple of the form :code:`(minx, miny, maxx, maxy)`
"""
p = self.points
if p.shape[1] > 0:
return np.min(p[0]), np.min(p[1]), np.max(p[0]), np.max(p[1])
else:
return 0, 0, 0, 0
@property
def size(self) -> Float2:
"""Size of the geom.
Returns:
Tuple of the form :code:`(sizex, sizey)`.
"""
b = self.bounds # (minx, miny, maxx, maxy)
return b[2] - b[0], b[3] - b[1] # (maxx - minx, maxy - miny)
@property
def bbox(self):
"""Opposing diagonal corners of the bounding box (bbox).
Returns:
A numpy array of bottom left and top right corners of bounding box (bbox).
"""
return np.reshape(self.bounds, (2, 2)).T
@property
def center(self) -> Float2:
"""Center of the geom.
Returns:
Center for the component.
"""
b = self.bounds # (minx, miny, maxx, maxy)
return (b[2] + b[0]) / 2, (b[3] + b[1]) / 2 # (avgx, avgy)
@property
def shapely(self):
raise NotImplementedError("This needs to be implemented by subclasses of Geometry.")
[docs] def set_port(self, port: Dict[str, Port]):
self.port = port
return self
[docs] def translate(self, dx: float = 0, dy: float = 0) -> "Geometry":
"""Translate patter
Args:
dx: Displacement in x
dy: Displacement in y
Returns:
The translated geom
"""
return self.transform(translate2d((dx, dy)))
[docs] def reflect(self, center: Float2 = (0, 0), horiz: bool = False) -> "Geometry":
"""Reflect the component across a center point (default (0, 0))
Args:
center: The center point about which to flip
horiz: do horizontal flip, otherwise vertical flip
Returns:
Flipped geom
"""
self.transform(reflect2d(center, horiz))
for name, port in self.port.items():
port.a = np.mod(port.a + 180, 360) # temp fix... need to change how ports are represented
return self
[docs] def rotate(self, angle: float, origin: Union[Float2, np.ndarray] = (0, 0)) -> "Geometry":
"""Rotate the geometry about :code:`origin`.
Args:
angle: rotation angle in degrees.
origin: origin of rotation.
Returns:
Rotated geom by :code:`angle` about :code:`origin`
"""
if angle % 360 != 0: # to save time, only rotate if the angle is nonzero
a = angle * np.pi / 180
self.transform(rotate2d(a, origin))
return self
[docs] def skew(self, xs: float = 0, ys: float = 0, origin: Optional[Float2] = None) -> "Geometry":
"""Affine skew operation on the geometry about :code:`origin`.
Args:
xs: x skew factor.
ys: y skew factor.
origin: origin of rotation (uses center of geom if :code:`None`).
Returns:
Rotated geom by :code:`angle` about :code:`origin`
"""
return self.transform(skew2d((xs, ys), self.center if origin is None else origin))
[docs] def scale(self, xfact: float = 1, yfact: float = None, origin: Optional[Float2] = None) -> "Geometry":
"""Affine scale operation on the geometry about :code:`origin`.
Args:
xfact: x scale factor.
yfact: y scale factor (same as x scale factor if not specified).
origin: origin of rotation (uses center of geom if :code:`None`).
Returns:
Rotated geom by :code:`angle` about :code:`origin`
"""
yfact = xfact if yfact is None else yfact
return self.transform(scale2d((xfact, yfact), self.center if origin is None else origin))
[docs] def to(self, port: Union[Tuple[float, ...], Port] = (0, 0), from_port: Optional[Union[str, Port]] = None):
port = Port(*port) if isinstance(port, (tuple, np.ndarray)) else port
from_port = Port(*from_port) if isinstance(from_port, tuple) else from_port
if from_port is None:
return self.rotate(port.a).translate(port.x, port.y)
fp = self.port[from_port] if isinstance(from_port, str) else from_port
return self.rotate(port.a - fp.a + 180, origin=fp.xy).translate(port.x - fp.x, port.y - fp.y)
[docs] def align(self, geom_or_center: Union["Geometry", Float2] = (0, 0),
other: Union["Geometry", Float2] = None) -> "Geometry":
"""Align center of geom
Args:
geom_or_center: A geom (align to the geom's center) or a center point for alignment.
other: If specified, instead of aligning based on this geom's center,
align based on another geom's center and translate accordingly.
Returns:
Aligned geom
"""
if other is None:
old_x, old_y = self.center
else:
old_x, old_y = other.center if isinstance(other, Geometry) else other
center = geom_or_center.center if isinstance(geom_or_center, Geometry) else geom_or_center
self.translate(center[0] - old_x, center[1] - old_y)
return self
[docs] def halign(self, c: Union["Geometry", float], left: bool = True, opposite: bool = False) -> "Geometry":
"""Horizontal alignment of geom
Args:
c: A geom (horizontal align to the geom's boundary) or a center x for alignment
left: (if :code:`c` is geom) Align to left boundary of component, otherwise right boundary
opposite: (if :code:`c` is geom) Align opposite faces (left-right, right-left)
Returns:
Horizontally aligned geom
"""
x = self.bounds[0] if left else self.bounds[2]
p = c if np.isscalar(c) \
else (c.bounds[0] if left and not opposite or opposite and not left else c.bounds[2])
self.translate(dx=p - x)
return self
[docs] def valign(self, c: Union["Geometry", float], bottom: bool = True, opposite: bool = False) -> "Geometry":
"""Vertical alignment of geom
Args:
c: A geom (vertical align to the geom's boundary) or a center y for alignment
bottom: (if :code:`c` is geom) Align to upper boundary of component, otherwise lower boundary
opposite: (if :code:`c` is geom) Align opposite faces (upper-lower, lower-upper)
Returns:
Vertically aligned geom
"""
y = self.bounds[1] if bottom else self.bounds[3]
p = c if np.isscalar(c) \
else (c.bounds[1] if bottom and not opposite or opposite and not bottom else c.bounds[3])
self.translate(dy=p - y)
return self
[docs] def vstack(self, other_geom: "Geometry", bottom: bool = False) -> "Geometry":
return self.align(other_geom).valign(other_geom, bottom=bottom, opposite=True)
[docs] def hstack(self, other_geom: "Geometry", left: bool = False) -> "Geometry":
return self.align(other_geom).halign(other_geom, left=left, opposite=True)
@property
def copy(self) -> "Geometry":
"""Copies the pattern using deepcopy.
Returns:
A copy of the Pattern so that changes do not propagate to the original :code:`Pattern`.
"""
return Geometry(self.geoms, self.port, self.refs, self.tangents)
[docs] def reverse(self) -> "Geometry":
"""Reverse the geometry and the direction of the tangents along the path.
Returns:
The reversed geometry.
"""
self.geoms = [g[:, ::-1] for g in self.geoms[::-1]]
self.tangents = [-t[:, ::-1] for t in self.tangents[::-1]]
self.refs = [r.reverse() for r in self.refs]
return self.flip_ends()
[docs] def symmetrized(self, front_port: str = 'b0') -> "Geometry":
"""Symmetrize this curve across a mirror plane decided by one of the curves in the curve set.
Args:
front_port: Front port label.
back_port: Back port label.
Returns:
The symmetrized curve
"""
d = self.port[front_port].tangent()
final_angle = np.arctan2(*d[::-1])
mirror = AffineTransform([reflect2d(self.port[front_port].xy),
rotate2d(2 * final_angle + np.pi, self.port[front_port].xy)]).transform
mirrored = self.copy.transform(mirror).reverse()
forward = self.copy
forward.geoms += mirrored.geoms
forward.tangents += mirrored.tangents
forward.port[front_port] = mirrored.port[front_port].flip().copy
forward.refs.extend(mirrored.refs)
forward.curve = self.curve.symmetrized() if self.curve is not None else None
return forward
[docs] def flip_ends(self, front_port: str = 'b0', back_port: str = 'a0'):
self.port = {
front_port: self.port[back_port],
back_port: self.port[front_port]
}
return self
def __hash__(self):
return hash(id(self.shapely.wkt))
@property
def port_copy(self):
"""The copy of ports in this device.
Note:
Whenever using the ports of a given geometry in another geometry, it is highly recommended
to extract :code:`port_copy`, which creates fresh copies of the ports.
Returns:
The port dictionary copy.
"""
return {name: p.copy for name, p in self.port.items()}