Compare commits
No commits in common. "cfec9e8c7651d40eac8dd1ad87bc16cf72fce1cd" and "db222373694a1911923c94c201cd41e39cc4cc46" have entirely different histories.
cfec9e8c76
...
db22237369
9 changed files with 38 additions and 234 deletions
|
|
@ -98,20 +98,18 @@ class Label(PositionableImpl, RepeatableImpl, AnnotatableImpl, Bounded, Pivotabl
|
||||||
"""
|
"""
|
||||||
pivot = numpy.asarray(pivot, dtype=float)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
self.translate(-pivot)
|
self.translate(-pivot)
|
||||||
if self.repetition is not None:
|
|
||||||
self.repetition.rotate(rotation)
|
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
self.translate(+pivot)
|
self.translate(+pivot)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
|
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation: Flip the label across a line in the pattern's
|
Flip the label across a line in the pattern's coordinate system.
|
||||||
coordinate system. This affects both the label's offset and its
|
|
||||||
repetition grid.
|
This operation mirrors the label's offset relative to the pattern's origin.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
axis: Axis to mirror across. 0: x-axis (flip y), 1: y-axis (flip x).
|
axis: Axis to mirror across. 0 mirrors across y=0. 1 mirrors across x=0.
|
||||||
x: Vertical line x=val to mirror across.
|
x: Vertical line x=val to mirror across.
|
||||||
y: Horizontal line y=val to mirror across.
|
y: Horizontal line y=val to mirror across.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Object representing a one multi-layer lithographic layout.
|
Object representing a one multi-layer lithographic layout.
|
||||||
A single level of hierarchical references is included.
|
A single level of hierarchical references is included.
|
||||||
"""
|
"""
|
||||||
from typing import cast, Self, Any, TypeVar
|
from typing import cast, Self, Any, TypeVar, TYPE_CHECKING
|
||||||
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
|
from collections.abc import Sequence, Mapping, MutableMapping, Iterable, Callable
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -25,6 +25,8 @@ from .error import PatternError, PortError
|
||||||
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
|
||||||
from .ports import Port, PortList
|
from .ports import Port, PortList
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .traits import Flippable
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -172,8 +174,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def __copy__(self) -> 'Pattern':
|
def __copy__(self) -> 'Pattern':
|
||||||
logger.warning('Making a shallow copy of a Pattern... old shapes/refs/labels are re-referenced! '
|
logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
|
||||||
'Consider using .deepcopy() if this was not intended.')
|
|
||||||
new = Pattern(
|
new = Pattern(
|
||||||
annotations=copy.deepcopy(self.annotations),
|
annotations=copy.deepcopy(self.annotations),
|
||||||
ports=copy.deepcopy(self.ports),
|
ports=copy.deepcopy(self.ports),
|
||||||
|
|
@ -746,9 +747,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation: Rotate the Pattern around the a location in the
|
Rotate the Pattern around the a location.
|
||||||
container's coordinate system. This affects all elements' offsets and
|
|
||||||
their repetition grids.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pivot: (x, y) location to rotate around
|
pivot: (x, y) location to rotate around
|
||||||
|
|
@ -767,9 +766,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
|
|
||||||
def rotate_element_centers(self, rotation: float) -> Self:
|
def rotate_element_centers(self, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation part: Rotate the offsets and repetition grids of all
|
Rotate the offsets of all shapes, labels, refs, and ports around (0, 0)
|
||||||
shapes, labels, refs, and ports around (0, 0) in the container's
|
|
||||||
coordinate system.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rotation: Angle to rotate by (counter-clockwise, radians)
|
rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
|
|
@ -780,15 +777,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
for entry in chain(chain_elements(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
|
old_offset = cast('Positionable', entry).offset
|
||||||
cast('Positionable', entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
cast('Positionable', entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||||
if isinstance(entry, Repeatable) and entry.repetition is not None:
|
|
||||||
entry.repetition.rotate(rotation)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rotate_elements(self, rotation: float) -> Self:
|
def rotate_elements(self, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Intrinsic transformation part: Rotate each shape, ref, label, and port around its
|
Rotate each shape, ref, and port around its origin (offset)
|
||||||
origin (offset) in the container's coordinate system. This does NOT
|
|
||||||
affect their repetition grids.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rotation: Angle to rotate by (counter-clockwise, radians)
|
rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
|
|
@ -796,32 +789,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||||
if isinstance(entry, Rotatable):
|
cast('Rotatable', entry).rotate(rotation)
|
||||||
entry.rotate(rotation)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mirror_element_centers(self, axis: int = 0) -> Self:
|
|
||||||
"""
|
|
||||||
Extrinsic transformation part: Mirror the offsets and repetition grids of all
|
|
||||||
shapes, labels, refs, and ports relative to the container's origin.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis to mirror across (0: x-axis, 1: y-axis)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
|
||||||
cast('Positionable', entry).offset[1 - axis] *= -1
|
|
||||||
if isinstance(entry, Repeatable) and entry.repetition is not None:
|
|
||||||
entry.repetition.mirror(axis)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_elements(self, axis: int = 0) -> Self:
|
def mirror_elements(self, axis: int = 0) -> Self:
|
||||||
"""
|
"""
|
||||||
Intrinsic transformation part: Mirror each shape, ref, label, and port relative
|
Mirror each shape, ref, and port relative to its offset.
|
||||||
to its offset. This does NOT affect their repetition grids.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
axis: Axis to mirror across
|
axis: Axis to mirror across
|
||||||
|
|
@ -831,16 +805,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||||
if isinstance(entry, Mirrorable):
|
cast('Mirrorable', entry).mirror(axis=axis)
|
||||||
entry.mirror(axis=axis)
|
|
||||||
self._log_bulk_update(f"mirror_elements({axis})")
|
self._log_bulk_update(f"mirror_elements({axis})")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self, axis: int = 0) -> Self:
|
def mirror(self, axis: int = 0) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation: Mirror the Pattern across an axis through its origin.
|
Mirror the Pattern across an axis through its origin.
|
||||||
This affects all elements' offsets and their internal orientations.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
axis: Axis to mirror across (0: x-axis, 1: y-axis).
|
axis: Axis to mirror across (0: x-axis, 1: y-axis).
|
||||||
|
|
@ -848,8 +820,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
self.mirror_elements(axis=axis)
|
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||||
self.mirror_element_centers(axis=axis)
|
cast('Flippable', entry).flip_across(axis=axis)
|
||||||
self._log_bulk_update(f"mirror({axis})")
|
self._log_bulk_update(f"mirror({axis})")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -861,7 +833,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
Returns:
|
Returns:
|
||||||
A deep copy of the current Pattern.
|
A deep copy of the current Pattern.
|
||||||
"""
|
"""
|
||||||
return self.deepcopy()
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
def deepcopy(self) -> Self:
|
def deepcopy(self) -> Self:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1196,7 +1168,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
for target, refs in pat.refs.items():
|
for target, refs in pat.refs.items():
|
||||||
if target is None:
|
if target is None:
|
||||||
continue
|
continue
|
||||||
assert library is not None
|
|
||||||
target_pat = library[target]
|
target_pat = library[target]
|
||||||
for ref in refs:
|
for ref in refs:
|
||||||
# Ref order of operations: mirror, rotate, scale, translate, repeat
|
# Ref order of operations: mirror, rotate, scale, translate, repeat
|
||||||
|
|
@ -1576,9 +1547,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
self._log_port_removal(ki)
|
self._log_port_removal(ki)
|
||||||
map_out[vi] = None
|
map_out[vi] = None
|
||||||
|
|
||||||
if isinstance(other, Pattern) and not (append or skip_geometry):
|
if isinstance(other, Pattern):
|
||||||
raise PatternError('Must provide an `Abstract` (not a `Pattern`) when creating a reference. '
|
assert append or skip_geometry, 'Got a name (not an abstract) but was asked to reference (not append)'
|
||||||
'Use `append=True` if you intended to append the full geometry.')
|
|
||||||
|
|
||||||
self.place(
|
self.place(
|
||||||
other,
|
other,
|
||||||
|
|
|
||||||
|
|
@ -166,11 +166,9 @@ class Ref(
|
||||||
return pattern
|
return pattern
|
||||||
|
|
||||||
def rotate(self, rotation: float) -> Self:
|
def rotate(self, rotation: float) -> Self:
|
||||||
"""
|
|
||||||
Intrinsic transformation: Rotate the target pattern relative to this Ref's
|
|
||||||
origin. This does NOT affect the repetition grid.
|
|
||||||
"""
|
|
||||||
self.rotation += rotation
|
self.rotation += rotation
|
||||||
|
if self.repetition is not None:
|
||||||
|
self.repetition.rotate(rotation)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self, axis: int = 0) -> Self:
|
def mirror(self, axis: int = 0) -> Self:
|
||||||
|
|
|
||||||
|
|
@ -397,8 +397,6 @@ class Arbitrary(Repetition):
|
||||||
Returns:
|
Returns:
|
||||||
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
`[[x_min, y_min], [x_max, y_max]]` or `None`
|
||||||
"""
|
"""
|
||||||
if self.displacements.size == 0:
|
|
||||||
return None
|
|
||||||
xy_min = numpy.min(self.displacements, axis=0)
|
xy_min = numpy.min(self.displacements, axis=0)
|
||||||
xy_max = numpy.max(self.displacements, axis=0)
|
xy_max = numpy.max(self.displacements, axis=0)
|
||||||
return numpy.array((xy_min, xy_max))
|
return numpy.array((xy_min, xy_max))
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
import numpy as np
|
|
||||||
from numpy.testing import assert_allclose
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..ref import Ref
|
|
||||||
from ..label import Label
|
|
||||||
from ..repetition import Grid
|
|
||||||
|
|
||||||
def test_ref_rotate_intrinsic() -> None:
|
|
||||||
# Intrinsic rotate() should NOT affect repetition
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
ref = Ref(repetition=rep)
|
|
||||||
|
|
||||||
ref.rotate(np.pi/2)
|
|
||||||
|
|
||||||
assert_allclose(ref.rotation, np.pi/2, atol=1e-10)
|
|
||||||
# Grid vector should still be (10, 0)
|
|
||||||
assert ref.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref.repetition).a_vector, [10, 0], atol=1e-10)
|
|
||||||
|
|
||||||
def test_ref_rotate_around_extrinsic() -> None:
|
|
||||||
# Extrinsic rotate_around() SHOULD affect repetition
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
ref = Ref(repetition=rep)
|
|
||||||
|
|
||||||
ref.rotate_around((0, 0), np.pi/2)
|
|
||||||
|
|
||||||
assert_allclose(ref.rotation, np.pi/2, atol=1e-10)
|
|
||||||
# Grid vector should be rotated to (0, 10)
|
|
||||||
assert ref.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref.repetition).a_vector, [0, 10], atol=1e-10)
|
|
||||||
|
|
||||||
def test_pattern_rotate_around_extrinsic() -> None:
|
|
||||||
# Pattern.rotate_around() SHOULD affect repetition of its elements
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
ref = Ref(repetition=rep)
|
|
||||||
|
|
||||||
pat = Pattern()
|
|
||||||
pat.refs['cell'].append(ref)
|
|
||||||
|
|
||||||
pat.rotate_around((0, 0), np.pi/2)
|
|
||||||
|
|
||||||
# Check the ref inside the pattern
|
|
||||||
ref_in_pat = pat.refs['cell'][0]
|
|
||||||
assert_allclose(ref_in_pat.rotation, np.pi/2, atol=1e-10)
|
|
||||||
# Grid vector should be rotated to (0, 10)
|
|
||||||
assert ref_in_pat.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [0, 10], atol=1e-10)
|
|
||||||
|
|
||||||
def test_label_rotate_around_extrinsic() -> None:
|
|
||||||
# Extrinsic rotate_around() SHOULD affect repetition of labels
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
lbl = Label("test", repetition=rep, offset=(5, 0))
|
|
||||||
|
|
||||||
lbl.rotate_around((0, 0), np.pi/2)
|
|
||||||
|
|
||||||
# Label offset should be (0, 5)
|
|
||||||
assert_allclose(lbl.offset, [0, 5], atol=1e-10)
|
|
||||||
# Grid vector should be rotated to (0, 10)
|
|
||||||
assert lbl.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', lbl.repetition).a_vector, [0, 10], atol=1e-10)
|
|
||||||
|
|
||||||
def test_pattern_rotate_elements_intrinsic() -> None:
|
|
||||||
# rotate_elements() should NOT affect repetition
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
ref = Ref(repetition=rep)
|
|
||||||
|
|
||||||
pat = Pattern()
|
|
||||||
pat.refs['cell'].append(ref)
|
|
||||||
|
|
||||||
pat.rotate_elements(np.pi/2)
|
|
||||||
|
|
||||||
ref_in_pat = pat.refs['cell'][0]
|
|
||||||
assert_allclose(ref_in_pat.rotation, np.pi/2, atol=1e-10)
|
|
||||||
# Grid vector should still be (10, 0)
|
|
||||||
assert ref_in_pat.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, 0], atol=1e-10)
|
|
||||||
|
|
||||||
def test_pattern_rotate_element_centers_extrinsic() -> None:
|
|
||||||
# rotate_element_centers() SHOULD affect repetition and offset
|
|
||||||
rep = Grid(a_vector=(10, 0), a_count=2)
|
|
||||||
ref = Ref(repetition=rep, offset=(5, 0))
|
|
||||||
|
|
||||||
pat = Pattern()
|
|
||||||
pat.refs['cell'].append(ref)
|
|
||||||
|
|
||||||
pat.rotate_element_centers(np.pi/2)
|
|
||||||
|
|
||||||
ref_in_pat = pat.refs['cell'][0]
|
|
||||||
# Offset should be (0, 5)
|
|
||||||
assert_allclose(ref_in_pat.offset, [0, 5], atol=1e-10)
|
|
||||||
# Grid vector should be rotated to (0, 10)
|
|
||||||
assert ref_in_pat.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [0, 10], atol=1e-10)
|
|
||||||
# Ref rotation should NOT be changed
|
|
||||||
assert_allclose(ref_in_pat.rotation, 0, atol=1e-10)
|
|
||||||
|
|
||||||
def test_pattern_mirror_elements_intrinsic() -> None:
|
|
||||||
# mirror_elements() should NOT affect repetition or offset
|
|
||||||
rep = Grid(a_vector=(10, 5), a_count=2)
|
|
||||||
ref = Ref(repetition=rep, offset=(5, 2))
|
|
||||||
|
|
||||||
pat = Pattern()
|
|
||||||
pat.refs['cell'].append(ref)
|
|
||||||
|
|
||||||
pat.mirror_elements(axis=0) # Mirror across x (flip y)
|
|
||||||
|
|
||||||
ref_in_pat = pat.refs['cell'][0]
|
|
||||||
assert ref_in_pat.mirrored is True
|
|
||||||
# Repetition and offset should be unchanged
|
|
||||||
assert ref_in_pat.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, 5], atol=1e-10)
|
|
||||||
assert_allclose(ref_in_pat.offset, [5, 2], atol=1e-10)
|
|
||||||
|
|
||||||
def test_pattern_mirror_element_centers_extrinsic() -> None:
|
|
||||||
# mirror_element_centers() SHOULD affect repetition and offset
|
|
||||||
rep = Grid(a_vector=(10, 5), a_count=2)
|
|
||||||
ref = Ref(repetition=rep, offset=(5, 2))
|
|
||||||
|
|
||||||
pat = Pattern()
|
|
||||||
pat.refs['cell'].append(ref)
|
|
||||||
|
|
||||||
pat.mirror_element_centers(axis=0) # Mirror across x (flip y)
|
|
||||||
|
|
||||||
ref_in_pat = pat.refs['cell'][0]
|
|
||||||
# Offset should be (5, -2)
|
|
||||||
assert_allclose(ref_in_pat.offset, [5, -2], atol=1e-10)
|
|
||||||
# Grid vector should be (10, -5)
|
|
||||||
assert ref_in_pat.repetition is not None
|
|
||||||
assert_allclose(cast('Grid', ref_in_pat.repetition).a_vector, [10, -5], atol=1e-10)
|
|
||||||
# Ref mirrored state should NOT be changed
|
|
||||||
assert ref_in_pat.mirrored is False
|
|
||||||
|
|
@ -18,8 +18,7 @@ class Mirrorable(metaclass=ABCMeta):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def mirror(self, axis: int = 0) -> Self:
|
def mirror(self, axis: int = 0) -> Self:
|
||||||
"""
|
"""
|
||||||
Intrinsic transformation: Mirror the entity across an axis through its origin.
|
Mirror the entity across an axis through its origin.
|
||||||
This does NOT affect the object's repetition grid.
|
|
||||||
|
|
||||||
This operation is performed relative to the object's internal origin (ignoring
|
This operation is performed relative to the object's internal origin (ignoring
|
||||||
its offset). For objects like `Polygon` and `Path` where the offset is forced
|
its offset). For objects like `Polygon` and `Path` where the offset is forced
|
||||||
|
|
@ -76,8 +75,7 @@ class Flippable(Positionable, metaclass=ABCMeta):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
|
def flip_across(self, axis: int | None = None, *, x: float | None = None, y: float | None = None) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation: Mirror the object across a line in the container's
|
Mirror the object across a line in the container's coordinate system.
|
||||||
coordinate system. This affects both the object's offset and its repetition grid.
|
|
||||||
|
|
||||||
Unlike `mirror()`, this operation is performed relative to the container's origin
|
Unlike `mirror()`, this operation is performed relative to the container's origin
|
||||||
(e.g. the `Pattern` origin, in the case of shapes) and takes the object's offset
|
(e.g. the `Pattern` origin, in the case of shapes) and takes the object's offset
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,7 @@ class Rotatable(metaclass=ABCMeta):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def rotate(self, val: float) -> Self:
|
def rotate(self, val: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Intrinsic transformation: Rotate the shape around its origin (0, 0), ignoring its offset.
|
Rotate the shape around its origin (0, 0), ignoring its offset.
|
||||||
This does NOT affect the object's repetition grid.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
val: Angle to rotate by (counterclockwise, radians)
|
val: Angle to rotate by (counterclockwise, radians)
|
||||||
|
|
@ -64,10 +63,6 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
||||||
# Methods
|
# Methods
|
||||||
#
|
#
|
||||||
def rotate(self, rotation: float) -> Self:
|
def rotate(self, rotation: float) -> Self:
|
||||||
"""
|
|
||||||
Intrinsic transformation: Rotate the shape around its origin (0, 0), ignoring its offset.
|
|
||||||
This does NOT affect the object's repetition grid.
|
|
||||||
"""
|
|
||||||
self.rotation += rotation
|
self.rotation += rotation
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -87,7 +82,7 @@ class RotatableImpl(Rotatable, metaclass=ABCMeta):
|
||||||
|
|
||||||
class Pivotable(Positionable, metaclass=ABCMeta):
|
class Pivotable(Positionable, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Trait class for entities which can be rotated around a point.
|
Trait class for entites which can be rotated around a point.
|
||||||
This requires that they are `Positionable` but not necessarily `Rotatable` themselves.
|
This requires that they are `Positionable` but not necessarily `Rotatable` themselves.
|
||||||
"""
|
"""
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
@ -95,11 +90,7 @@ class Pivotable(Positionable, metaclass=ABCMeta):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||||
"""
|
"""
|
||||||
Extrinsic transformation: Rotate the object around a point in the container's
|
Rotate the object around a point.
|
||||||
coordinate system. This affects both the object's offset and its repetition grid.
|
|
||||||
|
|
||||||
For objects that are also `Rotatable`, this also performs an intrinsic
|
|
||||||
rotation of the object.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pivot: Point (x, y) to rotate around
|
pivot: Point (x, y) to rotate around
|
||||||
|
|
@ -119,12 +110,9 @@ class PivotableImpl(Pivotable, Rotatable, metaclass=ABCMeta):
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||||
from .repeatable import Repeatable #noqa: PLC0415
|
|
||||||
pivot = numpy.asarray(pivot, dtype=float)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
self.translate(-pivot)
|
self.translate(-pivot)
|
||||||
self.rotate(rotation)
|
self.rotate(rotation)
|
||||||
if isinstance(self, Repeatable) and self.repetition is not None:
|
|
||||||
self.repetition.rotate(rotation)
|
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
self.translate(+pivot)
|
self.translate(+pivot)
|
||||||
return self
|
return self
|
||||||
|
|
|
||||||
|
|
@ -69,25 +69,14 @@ def euler_bend(
|
||||||
num_points_arc = num_points - 2 * num_points_spiral
|
num_points_arc = num_points - 2 * num_points_spiral
|
||||||
|
|
||||||
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
|
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
|
||||||
if ll_max == 0:
|
xx = []
|
||||||
return numpy.zeros((num_points_spiral, 2))
|
yy = []
|
||||||
|
for ll in numpy.linspace(0, ll_max, num_points_spiral):
|
||||||
resolution = 100000
|
qq = numpy.linspace(0, ll, 1000) # integrate to current arclength
|
||||||
qq = numpy.linspace(0, ll_max, resolution)
|
xx.append(trapezoid( numpy.cos(qq * qq / 2), qq))
|
||||||
dx = numpy.cos(qq * qq / 2)
|
yy.append(trapezoid(-numpy.sin(qq * qq / 2), qq))
|
||||||
dy = -numpy.sin(qq * qq / 2)
|
xy_part = numpy.stack((xx, yy), axis=1)
|
||||||
|
return xy_part
|
||||||
dq = ll_max / (resolution - 1)
|
|
||||||
ix = numpy.zeros(resolution)
|
|
||||||
iy = numpy.zeros(resolution)
|
|
||||||
ix[1:] = numpy.cumsum((dx[:-1] + dx[1:]) / 2) * dq
|
|
||||||
iy[1:] = numpy.cumsum((dy[:-1] + dy[1:]) / 2) * dq
|
|
||||||
|
|
||||||
ll_target = numpy.linspace(0, ll_max, num_points_spiral)
|
|
||||||
x_target = numpy.interp(ll_target, qq, ix)
|
|
||||||
y_target = numpy.interp(ll_target, qq, iy)
|
|
||||||
|
|
||||||
return numpy.stack((x_target, y_target), axis=1)
|
|
||||||
|
|
||||||
xy_spiral = gen_spiral(ll_max)
|
xy_spiral = gen_spiral(ll_max)
|
||||||
xy_parts = [xy_spiral]
|
xy_parts = [xy_spiral]
|
||||||
|
|
|
||||||
|
|
@ -236,9 +236,7 @@ def pack_patterns(
|
||||||
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
|
locations, reject_inds = packer(sizes, containers, presort=presort, allow_rejects=allow_rejects)
|
||||||
|
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
for ii, (pp, oo, loc) in enumerate(zip(patterns, offsets, locations, strict=True)):
|
for pp, oo, loc in zip(patterns, offsets, locations, strict=True):
|
||||||
if ii in reject_inds:
|
|
||||||
continue
|
|
||||||
pat.ref(pp, offset=oo + loc)
|
pat.ref(pp, offset=oo + loc)
|
||||||
|
|
||||||
rejects = [patterns[ii] for ii in reject_inds]
|
rejects = [patterns[ii] for ii in reject_inds]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue