Source code for simetri.graphics.pattern
from math import prod
from itertools import product
from dataclasses import dataclass
from hashlib import md5
import numpy as np
from typing_extensions import Union, Self
from .shape import Shape
from .batch import Batch
from .affine import *
from .common import Point, Line, common_properties
from .all_enums import Types, get_enum_value, Anchor
from .core import StyleMixin
from .bbox import bounding_box, BoundingBox
from ..canvas.style_map import ShapeStyle, shape_style_map, shape_args
from ..helpers.validation import validate_args
from ..geometry.geometry import homogenize
[docs]
@dataclass
class Transform:
"""
A class representing a single transformation.
Used in the Transformation class to represent a transformation matrix and its repetitions.
Attributes:
xform_matrix (ndarray): The transformation matrix.
reps (int): The number of repetitions of the transformation.
"""
xform_matrix: 'ndarray'
_reps: int = 0
def __post_init__(self):
self.type = Types.TRANSFORM
self.subtype = Types.TRANSFORM
common_properties(self, graphics_object=False, id_only=True)
self.__dict__['_xform_matrix'] = self.xform_matrix
# self.__dict__['_reps'] = self.reps
self._update()
def _update(self):
self.hash = md5(self.xform_matrix.tobytes()).hexdigest()
self._set_partitions()
self._composite = np.concatenate(self.partitions, axis=1)
@property
def reps(self) -> int:
return self._reps
@reps.setter
def reps(self, value: int):
if value < 0:
raise ValueError("x cannot be negative")
self._reps = value
def _changed(self):
"""
Checks if the transformation matrix has changed.
Returns:
bool: True if the transformation matrix has changed, False otherwise.
"""
return not((self.hash == md5(self.xform_matrix.tobytes()).hexdigest()) and
(self.reps == self._reps))
def _set_partitions(self):
if self.reps == 0:
partition_list = [identity_matrix()]
elif self.reps == 1:
partition_list = [identity_matrix(), self.xform_matrix]
else:
xform_mat = self.xform_matrix
partition_list = [identity_matrix(), xform_mat]
last = xform_mat
for _ in range(self._reps-1):
last = xform_mat @ last
partition_list.append(last)
self._partitions = partition_list
[docs]
def update(self):
self._set_partitions()
self._composite = np.concatenate(self.partitions, axis=1)
self.hash = md5(self._composition.tobytes()).hexdigest()
@property
def xform_matrix(self) -> 'ndarray':
"""
Returns the transformation matrix.
Returns:
ndarray: The transformation matrix.
"""
return self._xform_matrix
@xform_matrix.setter
def xform_matrix(self, value: 'ndarray'):
if not isinstance(value, np.ndarray):
raise ValueError("xform_matrix must be a numpy array")
self._xform_matrix = value
self._update()
@property
def partitions(self) -> list:
"""
Returns the submatrices in the transformation.
Returns:
list: A list of submatrices.
"""
if self._changed():
self.update()
return self._partitions
@partitions.setter
def partitions(self, value: list):
raise AttributeError(("Cannot set partitions directly. "
"Use the update method to update the partitions."))
@property
def composite(self) -> 'ndarray':
"""
Returns the compound transformation matrix.
Returns:
ndarray: The compound transformation matrix.
"""
if self._changed():
self.update()
return self._composite
@composite.setter
def composite(self, value: 'ndarray'):
raise AttributeError(("Cannot set composition directly. "
"Use the update method to update the composition."))
[docs]
def copy(self) -> 'Tranform':
"""
Creates a copy of the Transform instance.
Returns:
Transform: A new Transform instance with the same attributes.
"""
return Transform(self.xform_matrix.copy(), self.reps)
[docs]
@dataclass
class Transformation:
"""
A class representing a transformation that can be composite or not.
Attributes:
transforms (list): A list of Transform instances representing the transformations.
"""
components: list=None
def __post_init__(self):
self.type = Types.TRANSFORMATION
self.subtype = Types.TRANSFORMATION
if self.components is None:
self.components = []
common_properties(self, graphics_object=False, id_only=True)
@property
def partitions(self) -> list:
"""
Returns the submatrices in the transformation.
Returns:
list of ndarrays.
"""
if len(self.components) == 0:
return [identity_matrix()]
elif len(self.components) == 1:
partitions = [identity_matrix(), self.components[0].xform_matrix]
else:
partitions = []
for component in self.components:
partitions.extend(component.partitions)
return partitions
@property
def composite(self) -> 'ndarray':
"""
Returns the compound transformation matrix.
Returns:
ndarray: The compound transformation matrix.
"""
if len(self.components) == 0:
return identity_matrix()
matrices = []
for component in self.components:
matrices.append(component.partitions)
res = []
if len(matrices) == 1:
if len(matrices[0]) == 1:
return matrices[0][0]
else:
return np.concatenate(matrices[0], axis=1)
else:
for mats in product(*matrices):
res.append(np.linalg.multi_dot(mats))
return np.concatenate(res, axis=1)
@composite.setter
def composite(self, value: 'ndarray'):
raise AttributeError(("Cannot set composition directly. "
"Use the update method to update the composition."))
[docs]
def copy(self) -> 'Transformation':
"""
Creates a copy of the Transform instance.
Returns:
Transform: A new Transform instance with the same components.
"""
return Transformation([component.copy() for component in self.components])
[docs]
class Pattern(Batch, StyleMixin):
"""
A class representing a pattern of a shape or batch object.
Attributes:
kernel (Shape/Batch): The repeated form.
transformation: A Transformation object.
"""
def __init__(self, kernel: Union[Shape, Batch]=None, transformation:Transformation=None, **kwargs):
"""
Initializes the Pattern instance with a pattern and its count.
Args:
kernel (Shape/Batch): The repeated form of the pattern.
transformation (Transformation): The transformation applied to the pattern.
**kwargs: Additional keyword arguments.
"""
self.__dict__["style"] = ShapeStyle()
self.__dict__["_style_map"] = shape_style_map
self._set_aliases()
self.kernel = kernel
if transformation is None:
transformation = Transformation()
self.transformation = transformation
super().__init__(**kwargs)
self.subtype = Types.PATTERN
common_properties(self)
valid_args = shape_args
validate_args(kwargs, valid_args)
@property
def closed(self) -> bool:
"""
Returns True if the pattern is closed.
Returns:
bool: True if the pattern is closed, False otherwise.
"""
return self.kernel.closed
@closed.setter
def closed(self, value: bool):
"""
Sets the closed property of the pattern.
Args:
value (bool): True to set the pattern as closed, False otherwise.
"""
self.kernel.closed = value
@property
def composite(self) -> 'ndarray':
return self.transformation.composite
def __bool__(self):
return bool(self.kernel)
@property
def b_box(self) -> BoundingBox:
"""
Returns the bounding box of the pattern.
Returns:
BoundingBox: The bounding box of the pattern.
"""
vertices = self.get_all_vertices()
verts=np.hsplit(vertices, self.count)
res = []
for x in verts:
pass # comeback here later!!!!!!
return bounding_box(vertices)
[docs]
def get_vertices_list(self) -> list:
"""
Returns the submatrices of the transformation.
Returns:
list: A list of submatrices.
"""
return np.hsplit(self.get_all_vertices(), self.count)
[docs]
def get_shapes(self) -> Batch:
"""
Expands the pattern into a batch of shapes.
Returns:
Batch: A new Batch instance with the expanded shapes.
"""
vertices_list = self.get_vertices_list()
res = Batch()
for vertices in vertices_list:
res.append(Shape(vertices))
return res
@property
def count(self):
"""
Returns the number of occurrences of the pattern.
Returns:
int: The total number of forms in the pattern.
"""
return prod([comp.reps+1 for comp in self.transformation.components])
[docs]
def copy(self) -> 'Pattern':
"""
Creates a copy of the Pattern instance.
Returns:
Pattern: A new Pattern instance with the same attributes.
"""
kernel = None
if self.kernel is not None:
kernel = self.kernel.copy()
transformation = None
if self.transformation is not None:
transformation = self.transformation.copy()
pattern = Pattern(kernel, transformation)
for attrib in shape_style_map:
setattr(pattern, attrib, getattr(self, attrib))
return pattern
[docs]
def translate(self, dx: float = 0, dy: float = 0, reps: int = 0) -> Self:
"""
Translates the object by dx and dy.
Args:
dx (float): The translation distance along the x-axis.
dy (float): The translation distance along the y-axis.
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The transformed object.
"""
component = Transform(translation_matrix(dx, dy), reps)
self.transformation.components.append(component)
return self
[docs]
def rotate(self, angle: float, about: Point = (0, 0), reps: int = 0) -> Self:
"""
Rotates the object by the given angle (in radians) about the given point.
Args:
angle (float): The rotation angle in radians.
about (Point, optional): The point to rotate about. Defaults to (0, 0).
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The rotated object.
"""
component = Transform(rotation_matrix(angle, about), reps)
self.transformation.components.append(component)
return self
[docs]
def mirror(self, about: Union[Line, Point], reps: int = 0) -> Self:
"""
Mirrors the object about the given line or point.
Args:
about (Union[Line, Point]): The line or point to mirror about.
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The mirrored object.
"""
component = Transform(mirror_matrix(about), reps)
self.transformation.components.append(component)
return self
[docs]
def glide(self, glide_line: Line, glide_dist: float, reps: int = 0) -> Self:
"""
Glides (first mirror then translate) the object along the given line
by the given glide_dist.
Args:
glide_line (Line): The line to glide along.
glide_dist (float): The distance to glide.
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The glided object.
"""
component = Transform(glide_matrix(glide_line, glide_dist), reps)
self.transformation.components.append(component)
return self
[docs]
def scale(
self,
scale_x: float,
scale_y: Union[float, None] = None,
about: Point = (0, 0),
reps: int = 0,
) -> Self:
"""
Scales the object by the given scale factors about the given point.
Args:
scale_x (float): The scale factor in the x direction.
scale_y (float, optional): The scale factor in the y direction. Defaults to None.
about (Point, optional): The point to scale about. Defaults to (0, 0).
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The scaled object.
"""
if scale_y is None:
scale_y = scale_x
component = Transform(scale_in_place_matrix(scale_x, scale_y, about), reps)
self.transformation.components.append(component)
return self
[docs]
def shear(self, theta_x: float, theta_y: float, reps: int = 0) -> Self:
"""
Shears the object by the given angles.
Args:
theta_x (float): The shear angle in the x direction.
theta_y (float): The shear angle in the y direction.
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The sheared object.
"""
component = Transform(shear_matrix(theta_x, theta_y), reps)
self.transformation.components.append(component)
return self
[docs]
def transform(self, xform_matrix: 'ndarray', reps: int = 0) -> Self:
"""
Transforms the object by the given transformation matrix.
Args:
xform_matrix (ndarray): The transformation matrix.
reps (int, optional): The number of repetitions. Defaults to 0.
Returns:
Self: The transformed object.
"""
return self._update(xform_matrix, reps=reps)
[docs]
def move_to(self, pos: Point, anchor: Anchor = Anchor.CENTER) -> Self:
"""
Moves the object to the given position by using its center point.
Args:
pos (Point): The position to move to.
anchor (Anchor, optional): The anchor point. Defaults to Anchor.CENTER.
Returns:
Self: The moved object.
"""
x, y = pos[:2]
anchor = get_enum_value(Anchor, anchor)
x1, y1 = getattr(self.b_box, anchor)
component = Transform(translation_matrix(x - x1, y - y1), reps=0)
self.transformation.components.append(component)
return self