Compare commits

..

No commits in common. "fe231e558a31f68206349774e76385b8cd31a56d" and "00021c00e6fe9bdf29bbe48eaa5fd2a34a0c8d42" have entirely different histories.

17 changed files with 93 additions and 164 deletions

View File

@ -251,7 +251,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
for generating straight paths, and a table of pre-rendered `transitions` for converting for generating straight paths, and a table of pre-rendered `transitions` for converting
from non-native ptypes. from non-native ptypes.
""" """
straight: tuple[Callable[[float], Pattern] | Callable[[float], Library], str, str] straight: tuple[Callable[[float], Pattern], str, str]
""" `create_straight(length: float), in_port_name, out_port_name` """ """ `create_straight(length: float), in_port_name, out_port_name` """
bend: abstract_tuple_t # Assumed to be clockwise bend: abstract_tuple_t # Assumed to be clockwise
@ -290,16 +290,12 @@ class BasicTool(Tool, metaclass=ABCMeta):
gen_straight, sport_in, sport_out = self.straight gen_straight, sport_in, sport_out = self.straight
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) pat.add_port_pair(names=port_names, ptype=in_ptype)
if data.in_transition: if data.in_transition:
ipat, iport_theirs, _iport_ours = data.in_transition ipat, iport_theirs, _iport_ours = data.in_transition
pat.plug(ipat, {port_names[1]: iport_theirs}) pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(data.straight_length, 0): if not numpy.isclose(data.straight_length, 0):
straight_pat_or_tree = gen_straight(data.straight_length, **kwargs) straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
if isinstance(straight_pat_or_tree, Pattern):
straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat_or_tree}
else:
straight = tree <= straight_pat_or_tree
pat.plug(straight, {port_names[1]: sport_in}) pat.plug(straight, {port_names[1]: sport_in})
if data.ccw is not None: if data.ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
@ -409,24 +405,12 @@ class BasicTool(Tool, metaclass=ABCMeta):
ipat, iport_theirs, _iport_ours = in_transition ipat, iport_theirs, _iport_ours = in_transition
pat.plug(ipat, {port_names[1]: iport_theirs}) pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(straight_length, 0): if not numpy.isclose(straight_length, 0):
straight_pat_or_tree = gen_straight(straight_length, **kwargs) straight_pat = gen_straight(straight_length, **kwargs)
pmap = {port_names[1]: sport_in}
if isinstance(straight_pat_or_tree, Pattern):
straight_pat = straight_pat_or_tree
if append: if append:
pat.plug(straight_pat, pmap, append=True) pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
else: else:
straight_name = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat} straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat}
pat.plug(straight_name, pmap) pat.plug(straight, {port_names[1]: sport_in}, append=True)
else:
straight_tree = straight_pat_or_tree
if append:
top = straight_tree.top()
straight_tree.flatten(top)
pat.plug(straight_tree[top], pmap, append=True)
else:
straight = tree <= straight_pat_or_tree
pat.plug(straight, pmap)
if ccw is not None: if ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw)) pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))

View File

@ -418,8 +418,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
i = int(key) i = int(key)
except ValueError as err: except ValueError as err:
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err raise PatternError(f'Annotation key {key} is not convertable to an integer') from err
if not (0 < i <= 126): if not (0 < i < 126):
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,126])') raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
val_strings = ' '.join(str(val) for val in vals) val_strings = ' '.join(str(val) for val in vals)
b = val_strings.encode() b = val_strings.encode()

View File

@ -661,7 +661,7 @@ def repetition_masq2fata(
diffs = numpy.diff(rep.displacements, axis=0) diffs = numpy.diff(rep.displacements, axis=0)
diff_ints = rint_cast(diffs) diff_ints = rint_cast(diffs)
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
offset = tuple(rep.displacements[0, :]) offset = rep.displacements[0, :]
else: else:
assert rep is None assert rep is None
frep = None frep = None

View File

@ -584,7 +584,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
bounds = numpy.vstack((numpy.min(corners, axis=0), bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * ref.scale + [ref.offset] numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
if ref.repetition is not None: if ref.repetition is not None:
bounds += ref.repetition.get_bounds_nonempty() bounds += ref.repetition.get_bounds()
else: else:
# Non-manhattan rotation, have to figure out bounds by rotating the pattern # Non-manhattan rotation, have to figure out bounds by rotating the pattern

View File

@ -64,7 +64,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
return self._rotation return self._rotation
@rotation.setter @rotation.setter
def rotation(self, val: float | None) -> None: def rotation(self, val: float) -> None:
if val is None: if val is None:
self._rotation = None self._rotation = None
else: else:

View File

@ -11,7 +11,7 @@ import numpy
from numpy import pi from numpy import pi
from numpy.typing import NDArray, ArrayLike from numpy.typing import NDArray, ArrayLike
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key, SupportsBool from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
from .repetition import Repetition from .repetition import Repetition
from .traits import ( from .traits import (
PositionableImpl, RotatableImpl, ScalableImpl, PositionableImpl, RotatableImpl, ScalableImpl,
@ -50,11 +50,11 @@ class Ref(
# Mirrored property # Mirrored property
@property @property
def mirrored(self) -> bool: def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: SupportsBool) -> None: def mirrored(self, val: bool) -> None:
self._mirrored = bool(val) self._mirrored = bool(val)
def __init__( def __init__(

View File

@ -327,7 +327,7 @@ class Arbitrary(Repetition):
""" """
@property @property
def displacements(self) -> NDArray[numpy.float64]: def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
return self._displacements return self._displacements
@displacements.setter @displacements.setter

View File

@ -10,11 +10,10 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Arc(PositionableImpl, Shape): class Arc(Shape):
""" """
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
center. It has a position, two radii, a start and stop angle, a rotation, and a width. center. It has a position, two radii, a start and stop angle, a rotation, and a width.
@ -43,7 +42,7 @@ class Arc(PositionableImpl, Shape):
# radius properties # radius properties
@property @property
def radii(self) -> NDArray[numpy.float64]: def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Return the radii `[rx, ry]` Return the radii `[rx, ry]`
""" """
@ -80,7 +79,7 @@ class Arc(PositionableImpl, Shape):
# arc start/stop angle properties # arc start/stop angle properties
@property @property
def angles(self) -> NDArray[numpy.float64]: def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Return the start and stop angles `[a_start, a_stop]`. Return the start and stop angles `[a_start, a_stop]`.
Angles are measured from x-axis after rotation Angles are measured from x-axis after rotation
@ -413,15 +412,15 @@ class Arc(PositionableImpl, Shape):
start_angle -= pi start_angle -= pi
rotation += pi rotation += pi
norm_angles = (start_angle, start_angle + delta_angle) angles = (start_angle, start_angle + delta_angle)
rotation %= 2 * pi rotation %= 2 * pi
width = self.width width = self.width
return ((type(self), radii, norm_angles, width / norm_value), return ((type(self), radii, angles, width / norm_value),
(self.offset, scale / norm_value, rotation, False), (self.offset, scale / norm_value, rotation, False),
lambda: Arc( lambda: Arc(
radii=radii * norm_value, radii=radii * norm_value,
angles=norm_angles, angles=angles,
width=width * norm_value, width=width * norm_value,
)) ))

View File

@ -10,11 +10,10 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Circle(PositionableImpl, Shape): class Circle(Shape):
""" """
A circle, which has a position and radius. A circle, which has a position and radius.
""" """

View File

@ -11,11 +11,10 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
from ..traits import PositionableImpl
@functools.total_ordering @functools.total_ordering
class Ellipse(PositionableImpl, Shape): class Ellipse(Shape):
""" """
An ellipse, which has a position, two radii, and a rotation. An ellipse, which has a position, two radii, and a rotation.
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
@ -34,7 +33,7 @@ class Ellipse(PositionableImpl, Shape):
# radius properties # radius properties
@property @property
def radii(self) -> NDArray[numpy.float64]: def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Return the radii `[rx, ry]` Return the radii `[rx, ry]`
""" """

View File

@ -1,4 +1,4 @@
from typing import Any, cast, Self from typing import Any, cast
from collections.abc import Sequence from collections.abc import Sequence
import copy import copy
import functools import functools
@ -30,7 +30,8 @@ class PathCap(Enum):
@functools.total_ordering @functools.total_ordering
class Path(Shape): class Path(Shape):
""" """
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape. A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
and an offset.
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates. Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
@ -39,7 +40,7 @@ class Path(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_width', '_cap', '_cap_extensions', '_vertices', '_width', '_cap', '_cap_extensions',
# Inherited # Inherited
'_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
_width: float _width: float
@ -86,7 +87,7 @@ class Path(Shape):
# cap_extensions property # cap_extensions property
@property @property
def cap_extensions(self) -> NDArray[numpy.float64] | None: def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]:
""" """
Path end-cap extension Path end-cap extension
@ -112,7 +113,7 @@ class Path(Shape):
# vertices property # vertices property
@property @property
def vertices(self) -> NDArray[numpy.float64]: def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]:
""" """
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
@ -159,28 +160,6 @@ class Path(Shape):
raise PatternError('Wrong number of vertices') raise PatternError('Wrong number of vertices')
self.vertices[:, 1] = val self.vertices[:, 1] = val
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertices += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertices: ArrayLike, vertices: ArrayLike,
@ -198,8 +177,10 @@ class Path(Shape):
if raw: if raw:
assert isinstance(vertices, numpy.ndarray) assert isinstance(vertices, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
self._vertices = vertices self._vertices = vertices
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
self._width = width self._width = width
@ -207,19 +188,18 @@ class Path(Shape):
self._cap_extensions = cap_extensions self._cap_extensions = cap_extensions
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
self.width = width self.width = width
self.cap = cap self.cap = cap
self.cap_extensions = cap_extensions self.cap_extensions = cap_extensions
if numpy.any(offset):
self.translate(offset)
if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> 'Path': def __deepcopy__(self, memo: dict | None = None) -> 'Path':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new._cap = copy.deepcopy(self._cap, memo) new._cap = copy.deepcopy(self._cap, memo)
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
@ -229,6 +209,7 @@ class Path(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.vertices, other.vertices) and numpy.array_equal(self.vertices, other.vertices)
and self.width == other.width and self.width == other.width
and self.cap == other.cap and self.cap == other.cap
@ -253,6 +234,8 @@ class Path(Shape):
if self.cap_extensions is None: if self.cap_extensions is None:
return True return True
return tuple(self.cap_extensions) < tuple(other.cap_extensions) return tuple(self.cap_extensions) < tuple(other.cap_extensions)
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -309,7 +292,7 @@ class Path(Shape):
if self.width == 0: if self.width == 0:
verts = numpy.vstack((v, v[::-1])) verts = numpy.vstack((v, v[::-1]))
return [Polygon(vertices=verts)] return [Polygon(offset=self.offset, vertices=verts)]
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
@ -360,7 +343,7 @@ class Path(Shape):
o1.append(v[-1] - perp[-1]) o1.append(v[-1] - perp[-1])
verts = numpy.vstack((o0, o1[::-1])) verts = numpy.vstack((o0, o1[::-1]))
polys = [Polygon(vertices=verts)] polys = [Polygon(offset=self.offset, vertices=verts)]
if self.cap == PathCap.Circle: if self.cap == PathCap.Circle:
#for vert in v: # not sure if every vertex, or just ends? #for vert in v: # not sure if every vertex, or just ends?
@ -372,7 +355,7 @@ class Path(Shape):
def get_bounds_single(self) -> NDArray[numpy.float64]: def get_bounds_single(self) -> NDArray[numpy.float64]:
if self.cap == PathCap.Circle: if self.cap == PathCap.Circle:
bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
numpy.max(self.vertices, axis=0) + self.width / 2)) numpy.max(self.vertices, axis=0) + self.width / 2))
elif self.cap in ( elif self.cap in (
PathCap.Flush, PathCap.Flush,
@ -407,7 +390,7 @@ class Path(Shape):
def normalized_form(self, norm_value: float) -> normalized_shape_tuple: def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
# Note: this function is going to be pretty slow for many-vertexed paths, relative to # Note: this function is going to be pretty slow for many-vertexed paths, relative to
# other shapes # other shapes
offset = self.vertices.mean(axis=0) offset = self.vertices.mean(axis=0) + self.offset
zeroed_vertices = self.vertices - offset zeroed_vertices = self.vertices - offset
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
@ -477,5 +460,5 @@ class Path(Shape):
return extensions return extensions
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.vertices.mean(axis=0) centroid = self.offset + self.vertices.mean(axis=0)
return f'<Path 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}>'

View File

@ -10,7 +10,6 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from .polygon import Polygon from .polygon import Polygon
from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t
@ -28,7 +27,7 @@ class PolyCollection(Shape):
'_vertex_lists', '_vertex_lists',
'_vertex_offsets', '_vertex_offsets',
# Inherited # Inherited
'_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_vertex_lists: NDArray[numpy.float64] _vertex_lists: NDArray[numpy.float64]
@ -38,14 +37,14 @@ class PolyCollection(Shape):
""" 1D NDArray specifying the starting offset for each polygon """ """ 1D NDArray specifying the starting offset for each polygon """
@property @property
def vertex_lists(self) -> NDArray[numpy.float64]: def vertex_lists(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Vertices of the polygons, ((N+M+...) x 2). Use with `vertex_offsets`. Vertices of the polygons, ((N+M+...) x 2). Use with `vertex_offsets`.
""" """
return self._vertex_lists return self._vertex_lists
@property @property
def vertex_offsets(self) -> NDArray[numpy.intp]: def vertex_offsets(self) -> Any: # mypy#3004 NDArray[numpy.intp]:
""" """
Starting offset (in `vertex_lists`) for each polygon Starting offset (in `vertex_lists`) for each polygon
""" """
@ -68,27 +67,6 @@ class PolyCollection(Shape):
for slc in self.vertex_slices: for slc in self.vertex_slices:
yield self._vertex_lists[slc] yield self._vertex_lists[slc]
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
raise PatternError('PolyCollection offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertex_lists += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertex_lists: ArrayLike, vertex_lists: ArrayLike,
@ -103,23 +81,25 @@ class PolyCollection(Shape):
if raw: if raw:
assert isinstance(vertex_lists, numpy.ndarray) assert isinstance(vertex_lists, numpy.ndarray)
assert isinstance(vertex_offsets, numpy.ndarray) assert isinstance(vertex_offsets, numpy.ndarray)
assert isinstance(offset, numpy.ndarray)
self._vertex_lists = vertex_lists self._vertex_lists = vertex_lists
self._vertex_offsets = vertex_offsets self._vertex_offsets = vertex_offsets
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
else: else:
self._vertex_lists = numpy.asarray(vertex_lists, dtype=float) self._vertex_lists = numpy.asarray(vertex_lists, dtype=float)
self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp) self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp)
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
if numpy.any(offset):
self.translate(offset)
if rotation: if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> Self: def __deepcopy__(self, memo: dict | None = None) -> Self:
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertex_lists = self._vertex_lists.copy() new._vertex_lists = self._vertex_lists.copy()
new._vertex_offsets = self._vertex_offsets.copy() new._vertex_offsets = self._vertex_offsets.copy()
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
@ -128,6 +108,7 @@ class PolyCollection(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self._vertex_lists, other._vertex_lists) and numpy.array_equal(self._vertex_lists, other._vertex_lists)
and numpy.array_equal(self._vertex_offsets, other._vertex_offsets) and numpy.array_equal(self._vertex_offsets, other._vertex_offsets)
and self.repetition == other.repetition and self.repetition == other.repetition
@ -153,6 +134,8 @@ class PolyCollection(Shape):
return vv.shape[0] < oo.shape[0] return vv.shape[0] < oo.shape[0]
if len(self.vertex_lists) != len(other.vertex_lists): if len(self.vertex_lists) != len(other.vertex_lists):
return len(self.vertex_lists) < len(other.vertex_lists) return len(self.vertex_lists) < len(other.vertex_lists)
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -164,13 +147,14 @@ class PolyCollection(Shape):
) -> list['Polygon']: ) -> list['Polygon']:
return [Polygon( return [Polygon(
vertices = vv, vertices = vv,
offset = self.offset,
repetition = copy.deepcopy(self.repetition), repetition = copy.deepcopy(self.repetition),
annotations = copy.deepcopy(self.annotations), annotations = copy.deepcopy(self.annotations),
) for vv in self.polygon_vertices] ) for vv in self.polygon_vertices]
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
return numpy.vstack((numpy.min(self._vertex_lists, axis=0), return numpy.vstack((self.offset + numpy.min(self._vertex_lists, axis=0),
numpy.max(self._vertex_lists, axis=0))) self.offset + numpy.max(self._vertex_lists, axis=0)))
def rotate(self, theta: float) -> Self: def rotate(self, theta: float) -> Self:
if theta != 0: if theta != 0:
@ -191,7 +175,7 @@ class PolyCollection(Shape):
# other shapes # other shapes
meanv = self._vertex_lists.mean(axis=0) meanv = self._vertex_lists.mean(axis=0)
zeroed_vertices = self._vertex_lists - [meanv] zeroed_vertices = self._vertex_lists - [meanv]
offset = meanv offset = meanv + self.offset
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale normed_vertices = zeroed_vertices / scale
@ -219,5 +203,5 @@ class PolyCollection(Shape):
) )
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.vertex_lists.mean(axis=0) centroid = self.offset + self.vertex_lists.mean(axis=0)
return f'<PolyCollection centroid {centroid} p{len(self.vertex_offsets)}>' return f'<PolyCollection centroid {centroid} p{len(self.vertex_offsets)}>'

View File

@ -1,4 +1,4 @@
from typing import Any, cast, TYPE_CHECKING, Self from typing import Any, cast, TYPE_CHECKING
import copy import copy
import functools import functools
@ -20,7 +20,7 @@ if TYPE_CHECKING:
class Polygon(Shape): class Polygon(Shape):
""" """
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary. implicitly-closed boundary, and an offset.
Note that the setter for `Polygon.vertices` creates a copy of the Note that the setter for `Polygon.vertices` creates a copy of the
passed vertex coordinates. passed vertex coordinates.
@ -30,7 +30,7 @@ class Polygon(Shape):
__slots__ = ( __slots__ = (
'_vertices', '_vertices',
# Inherited # Inherited
'_repetition', '_annotations', '_offset', '_repetition', '_annotations',
) )
_vertices: NDArray[numpy.float64] _vertices: NDArray[numpy.float64]
@ -38,7 +38,7 @@ class Polygon(Shape):
# vertices property # vertices property
@property @property
def vertices(self) -> NDArray[numpy.float64]: def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
@ -85,28 +85,6 @@ class Polygon(Shape):
raise PatternError('Wrong number of vertices') raise PatternError('Wrong number of vertices')
self.vertices[:, 1] = val self.vertices[:, 1] = val
# Offset property for `Positionable`
@property
def offset(self) -> NDArray[numpy.float64]:
"""
[x, y] offset
"""
return numpy.zeros(2)
@offset.setter
def offset(self, val: ArrayLike) -> None:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
def set_offset(self, val: ArrayLike) -> Self:
if numpy.any(val):
raise PatternError('Path offset is forced to (0, 0)')
return self
def translate(self, offset: ArrayLike) -> Self:
self._vertices += numpy.atleast_2d(offset)
return self
def __init__( def __init__(
self, self,
vertices: ArrayLike, vertices: ArrayLike,
@ -121,20 +99,21 @@ class Polygon(Shape):
assert isinstance(vertices, numpy.ndarray) assert isinstance(vertices, numpy.ndarray)
assert isinstance(offset, numpy.ndarray) assert isinstance(offset, numpy.ndarray)
self._vertices = vertices self._vertices = vertices
self._offset = offset
self._repetition = repetition self._repetition = repetition
self._annotations = annotations self._annotations = annotations
else: else:
self.vertices = vertices self.vertices = vertices
self.offset = offset
self.repetition = repetition self.repetition = repetition
self.annotations = annotations self.annotations = annotations
if numpy.any(offset):
self.translate(offset)
if rotation: if rotation:
self.rotate(rotation) self.rotate(rotation)
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon': def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self) new = copy.copy(self)
new._offset = self._offset.copy()
new._vertices = self._vertices.copy() new._vertices = self._vertices.copy()
new._annotations = copy.deepcopy(self._annotations) new._annotations = copy.deepcopy(self._annotations)
return new return new
@ -142,6 +121,7 @@ class Polygon(Shape):
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
return ( return (
type(self) is type(other) type(self) is type(other)
and numpy.array_equal(self.offset, other.offset)
and numpy.array_equal(self.vertices, other.vertices) and numpy.array_equal(self.vertices, other.vertices)
and self.repetition == other.repetition and self.repetition == other.repetition
and annotations_eq(self.annotations, other.annotations) and annotations_eq(self.annotations, other.annotations)
@ -161,6 +141,8 @@ class Polygon(Shape):
if eq_lt_masked.size > 0: if eq_lt_masked.size > 0:
return eq_lt_masked.flat[0] return eq_lt_masked.flat[0]
return self.vertices.shape[0] < other.vertices.shape[0] return self.vertices.shape[0] < other.vertices.shape[0]
if not numpy.array_equal(self.offset, other.offset):
return tuple(self.offset) < tuple(other.offset)
if self.repetition != other.repetition: if self.repetition != other.repetition:
return rep2key(self.repetition) < rep2key(other.repetition) return rep2key(self.repetition) < rep2key(other.repetition)
return annotations_lt(self.annotations, other.annotations) return annotations_lt(self.annotations, other.annotations)
@ -266,11 +248,11 @@ class Polygon(Shape):
elif xmax is None: elif xmax is None:
assert xmin is not None assert xmin is not None
assert xctr is not None assert xctr is not None
lx = 2.0 * (xctr - xmin) lx = 2 * (xctr - xmin)
elif xmin is None: elif xmin is None:
assert xctr is not None assert xctr is not None
assert xmax is not None assert xmax is not None
lx = 2.0 * (xmax - xctr) lx = 2 * (xmax - xctr)
else: else:
raise PatternError('Two of xmin, xctr, xmax, lx must be None!') raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
else: # noqa: PLR5501 else: # noqa: PLR5501
@ -296,11 +278,11 @@ class Polygon(Shape):
elif ymax is None: elif ymax is None:
assert ymin is not None assert ymin is not None
assert yctr is not None assert yctr is not None
ly = 2.0 * (yctr - ymin) ly = 2 * (yctr - ymin)
elif ymin is None: elif ymin is None:
assert yctr is not None assert yctr is not None
assert ymax is not None assert ymax is not None
ly = 2.0 * (ymax - yctr) ly = 2 * (ymax - yctr)
else: else:
raise PatternError('Two of ymin, yctr, ymax, ly must be None!') raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
else: # noqa: PLR5501 else: # noqa: PLR5501
@ -381,8 +363,8 @@ class Polygon(Shape):
return [copy.deepcopy(self)] return [copy.deepcopy(self)]
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
return numpy.vstack((numpy.min(self.vertices, axis=0), return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
numpy.max(self.vertices, axis=0))) self.offset + numpy.max(self.vertices, axis=0)))
def rotate(self, theta: float) -> 'Polygon': def rotate(self, theta: float) -> 'Polygon':
if theta != 0: if theta != 0:
@ -402,7 +384,7 @@ class Polygon(Shape):
# other shapes # other shapes
meanv = self.vertices.mean(axis=0) meanv = self.vertices.mean(axis=0)
zeroed_vertices = self.vertices - meanv zeroed_vertices = self.vertices - meanv
offset = meanv offset = meanv + self.offset
scale = zeroed_vertices.std() scale = zeroed_vertices.std()
normed_vertices = zeroed_vertices / scale normed_vertices = zeroed_vertices / scale
@ -456,5 +438,5 @@ class Polygon(Shape):
return self return self
def __repr__(self) -> str: def __repr__(self) -> str:
centroid = self.vertices.mean(axis=0) centroid = self.offset + self.vertices.mean(axis=0)
return f'<Polygon centroid {centroid} v{len(self.vertices)}>' return f'<Polygon centroid {centroid} v{len(self.vertices)}>'

View File

@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
from ..traits import ( from ..traits import (
Rotatable, Mirrorable, Copyable, Scalable, Rotatable, Mirrorable, Copyable, Scalable,
Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl, PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[
DEFAULT_POLY_NUM_VERTICES = 24 DEFAULT_POLY_NUM_VERTICES = 24
class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable, class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta): PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
""" """
Class specifying functions common to all shapes. Class specifying functions common to all shapes.
@ -134,7 +134,7 @@ class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
mins, maxs = bounds mins, maxs = bounds
vertex_lists = [] vertex_lists = []
p_verts = polygon.vertices p_verts = polygon.vertices + polygon.offset
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True): for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
dv = v_next - v dv = v_next - v
@ -282,7 +282,7 @@ class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
offset = (numpy.where(keep_x)[0][0], offset = (numpy.where(keep_x)[0][0],
numpy.where(keep_y)[0][0]) numpy.where(keep_y)[0][0])
rastered = float_raster.raster((polygon.vertices).T, gx, gy) rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy)
binary_rastered = (numpy.abs(rastered) >= 0.5) binary_rastered = (numpy.abs(rastered) >= 0.5)
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)

View File

@ -9,8 +9,8 @@ from numpy.typing import NDArray, ArrayLike
from . import Shape, Polygon, normalized_shape_tuple from . import Shape, Polygon, normalized_shape_tuple
from ..error import PatternError from ..error import PatternError
from ..repetition import Repetition from ..repetition import Repetition
from ..traits import PositionableImpl, RotatableImpl from ..traits import RotatableImpl
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
# Loaded on use: # Loaded on use:
# from freetype import Face # from freetype import Face
@ -18,7 +18,7 @@ from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotatio
@functools.total_ordering @functools.total_ordering
class Text(PositionableImpl, RotatableImpl, Shape): class Text(RotatableImpl, Shape):
""" """
Text (to be printed e.g. as a set of polygons). Text (to be printed e.g. as a set of polygons).
This is distinct from non-printed Label objects. This is distinct from non-printed Label objects.
@ -55,11 +55,11 @@ class Text(PositionableImpl, RotatableImpl, Shape):
self._height = val self._height = val
@property @property
def mirrored(self) -> bool: def mirrored(self) -> bool: # mypy#3004, should be bool
return self._mirrored return self._mirrored
@mirrored.setter @mirrored.setter
def mirrored(self, val: SupportsBool) -> None: def mirrored(self, val: bool) -> None:
self._mirrored = bool(val) self._mirrored = bool(val)
def __init__( def __init__(
@ -201,7 +201,7 @@ def get_char_as_polygons(
font_path: str, font_path: str,
char: str, char: str,
resolution: float = 48 * 64, resolution: float = 48 * 64,
) -> tuple[list[NDArray[numpy.float64]], float]: ) -> tuple[list[list[list[float]]], float]:
from freetype import Face # type: ignore from freetype import Face # type: ignore
from matplotlib.path import Path # type: ignore from matplotlib.path import Path # type: ignore
@ -276,12 +276,11 @@ def get_char_as_polygons(
advance = slot.advance.x / resolution advance = slot.advance.x / resolution
polygons: list[NDArray[numpy.float64]]
if len(all_verts) == 0: if len(all_verts) == 0:
polygons = [] polygons = []
else: else:
path = Path(all_verts, all_codes) path = Path(all_verts, all_codes)
path.should_simplify = False path.should_simplify = False
polygons = [numpy.asarray(poly) for poly in path.to_polygons()] polygons = path.to_polygons()
return polygons, advance return polygons, advance

View File

@ -73,7 +73,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
# #
# offset property # offset property
@property @property
def offset(self) -> NDArray[numpy.float64]: def offset(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
[x, y] offset [x, y] offset
""" """
@ -95,7 +95,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
return self return self
def translate(self, offset: ArrayLike) -> Self: def translate(self, offset: ArrayLike) -> Self:
self._offset += numpy.asarray(offset) self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
return self return self

View File

@ -116,7 +116,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
pivot = numpy.asarray(pivot, dtype=float) pivot = numpy.asarray(pivot, dtype=float)
cast('Positionable', self).translate(-pivot) cast('Positionable', self).translate(-pivot)
cast('Rotatable', self).rotate(rotation) cast('Rotatable', self).rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
cast('Positionable', self).translate(+pivot) cast('Positionable', self).translate(+pivot)
return self return self