masque/masque/traits/rotatable.py
jan 5f91bd9c6c [BREAKING][Ref / Label / Pattern] Make rotate/mirror consistent intrinsic transfomations
offset and repetition are extrinsic; use rotate_around() and flip() to
alter both
mirror() and rotate() only affect the object's intrinsic properties
2026-03-09 23:34:39 -07:00

131 lines
3.4 KiB
Python

from typing import Self
from abc import ABCMeta, abstractmethod
import numpy
from numpy import pi
from numpy.typing import ArrayLike
from ..error import MasqueError
from ..utils import rotation_matrix_2d
from .positionable import Positionable
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
class Rotatable(metaclass=ABCMeta):
"""
Trait class for all rotatable entities
"""
__slots__ = ()
#
# Methods
#
@abstractmethod
def rotate(self, val: float) -> Self:
"""
Intrinsic transformation: Rotate the shape around its origin (0, 0), ignoring its offset.
This does NOT affect the object's repetition grid.
Args:
val: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pass
class RotatableImpl(Rotatable, metaclass=ABCMeta):
"""
Simple implementation of `Rotatable`
"""
__slots__ = _empty_slots
_rotation: float
""" rotation for the object, radians counterclockwise """
#
# Properties
#
@property
def rotation(self) -> float:
""" Rotation, radians counterclockwise """
return self._rotation
@rotation.setter
def rotation(self, val: float) -> None:
if not numpy.size(val) == 1:
raise MasqueError('Rotation must be a scalar')
self._rotation = val % (2 * pi)
#
# Methods
#
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
return self
def set_rotation(self, rotation: float) -> Self:
"""
Set the rotation to a value
Args:
rotation: radians ccw
Returns:
self
"""
self.rotation = rotation
return self
class Pivotable(Positionable, metaclass=ABCMeta):
"""
Trait class for entities which can be rotated around a point.
This requires that they are `Positionable` but not necessarily `Rotatable` themselves.
"""
__slots__ = ()
@abstractmethod
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
"""
Extrinsic transformation: Rotate the object around a point in the container's
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:
pivot: Point (x, y) to rotate around
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pass
class PivotableImpl(Pivotable, Rotatable, metaclass=ABCMeta):
"""
Implementation of `Pivotable` for objects which are `Rotatable`
and `Positionable`.
"""
__slots__ = ()
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
from .repeatable import Repeatable #noqa: PLC0415
pivot = numpy.asarray(pivot, dtype=float)
self.translate(-pivot)
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.translate(+pivot)
return self