####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
####################################################################################################
r"""Module to implement transformations like scale, rotation and translation.
For resources on transformations see :ref:`this section `.
"""
####################################################################################################
__all__ = [
'TransformationType',
'Transformation',
'Transformation2D',
'AffineTransformation',
'AffineTransformation2D',
]
####################################################################################################
from enum import Enum, auto
from math import sin, cos, radians, degrees
import numpy as np
from .Vector import Vector2D, HomogeneousVector2D
####################################################################################################
class TransformationType(Enum):
Identity = auto()
Scale = auto() # same scale factor across axes
Shear = auto() # different scale factor
Parity = auto()
XParity = auto()
YParity = auto()
Rotation = auto()
Translation = auto()
Generic = auto()
####################################################################################################
class IncompatibleArrayDimension(ValueError):
pass
####################################################################################################
class Transformation:
__dimension__ = None
__size__ = None
##############################################
@classmethod
def Identity(cls):
return cls(np.identity(cls.__size__), TransformationType.Identity)
##############################################
def __init__(self, obj, transformation_type=TransformationType.Generic):
if isinstance(obj, Transformation):
if self.same_dimension(obj):
array = obj.array # *._m
else:
raise IncompatibleArrayDimension
elif isinstance(obj, np.ndarray):
if obj.shape == (self.__size__, self.__size__):
array = obj
else:
raise IncompatibleArrayDimension
elif isinstance(obj, (list, tuple)):
if len(obj) == self.__size__ **2:
array = np.array(obj)
array.shape = (self.__size__, self.__size__)
else:
raise IncompatibleArrayDimension
else:
array = np.array((self.__size__, self.__size__))
array[...] = obj
self._m = np.array(array)
if transformation_type == TransformationType.Generic:
transformation_type = self._check_type()
self._type = transformation_type
##############################################
@property
def dimension(self):
return self.__dimension__
@property
def size(self):
return self._size
@property
def array(self):
return self._m
@property
def type(self):
return self._type
@property
def is_identity(self):
return self._type == TransformationType.Identity
##############################################
def __repr__(self):
return self.__class__.__name__ + str(self._m)
##############################################
def to_list(self):
return list(self._m.flat)
##############################################
def same_dimension(self, other):
return self.__size__ == other.dimension
##############################################
def _check_type(self):
raise NotImplementedError
##############################################
def _mul_type(self, obj):
# Fixme: check matrix value ???
# usage identity/rotation, scale/parity test
# metric test ?
# if t in (parity, xparity, yparity) t*t = Id
# if t in (rotation, scale) t*t = t
if self._type == obj._type:
if self._type in (
TransformationType.Parity,
TransformationType.XParity,
TransformationType.YParity
):
return TransformationType.Identity
elif self._type not in (TransformationType.Rotation, TransformationType.Scale):
return TransformationType.Generic
else:
return self._type
else: # shear, generic
return TransformationType.Generic
#######################################
def __mul__(self, obj):
"""Return self * obj composition."""
if isinstance(obj, Transformation):
# T = T1 * T2
array = np.matmul(self._m, obj.array)
return self.__class__(array, self._mul_type(obj))
elif isinstance(obj, Vector2D):
array = np.matmul(self._m, np.transpose(obj.v))
return Vector2D(array)
elif isinstance(obj, (int, float)):
# Scalar can only be scaled if the frame is not sheared
if self._type in (TransformationType.Identity, TransformationType.Rotation):
return obj
elif self._type not in (TransformationType.Shear, TransformationType.Generic):
return abs(self._m[0,0]) * obj
else:
raise ValueError('Transformation is sheared')
else:
raise ValueError
#######################################
def __imul__(self, obj):
"""Set transformation to obj * self composition."""
# Fixme: order ???
if isinstance(obj, Transformation):
if obj.type != TransformationType.Identity:
# (T = T1) *= T2
# T = T2 * T1
# order is inverted !
self._m = np.matmul(obj.array, self._m)
self._type = self._mul_type(obj)
if self._type == TransformationType.Generic:
self._type = self._check_type()
else:
raise ValueError
return self
####################################################################################################
class Transformation2D(Transformation):
__dimension__ = 2
__size__ = 2
##############################################
@classmethod
def type_for_scale(cls, x_scale, y_scale):
if x_scale == y_scale:
if x_scale == 1:
transformation_type = TransformationType.Identity
elif x_scale == -1:
transformation_type = TransformationType.Parity
else:
transformation_type = TransformationType.Scale
else:
if x_scale == -1 and y_scale == 1:
transformation_type = TransformationType.XParity
elif x_scale == 1 and y_scale == -1:
transformation_type = TransformationType.YParity
else:
transformation_type = TransformationType.Shear
return transformation_type
##############################################
@classmethod
def check_matrix_type(self, matrix):
# Fixme: check
m00, m01, m10, m11 = matrix
if m01 == 0 and m10 == 0:
if m00 == 1:
if m11 == 1:
return TransformationType.Identity
elif m11 == -1:
return TransformationType.YParity
elif m00 == -1:
if m11 == 1:
return TransformationType.XParity
elif m11 == -1:
return TransformationType.Parity
elif m00 == m11:
return TransformationType.Scale
# Fixme: check for rotation
return TransformationType.Generic
##############################################
def _check_type(self):
return self._check_matrix_type(self.to_list())
##############################################
@classmethod
def Rotation(cls, angle):
angle = radians(angle)
c = cos(angle)
s = sin(angle)
return cls(
np.array((
(c, -s),
(s, c))),
TransformationType.Rotation,
)
##############################################
@classmethod
def Scale(cls, x_scale, y_scale=None):
if y_scale is None:
y_scale = x_scale
transformation_type = cls.type_for_scale(x_scale, y_scale)
return cls(np.array(((x_scale, 0), (0, y_scale))), transformation_type)
##############################################
@classmethod
def Parity(cls):
return cls.Scale(-1, -1)
##############################################
@classmethod
def XReflection(cls):
return cls.Scale(-1, 1)
##############################################
@classmethod
def YReflection(cls):
return cls.Scale(1, -1)
####################################################################################################
class AffineTransformation(Transformation):
##############################################
@classmethod
def Translation(cls, vector):
transformation = cls.Identity()
transformation.translation_part[...] = vector.v[...]
transformation._type = TransformationType.Translation
return transformation
##############################################
@classmethod
def RotationAt(cls, center, angle):
# return cls.Translation(center) * cls.Rotation(angle) * cls.Translation(-center)
transformation = cls.Translation(-center)
transformation *= cls.Rotation(angle)
transformation *= cls.Translation(center)
return transformation
##############################################
@property
def matrix_part(self):
return self._m[:self.__dimension__,:self.__dimension__]
@property
def translation_part(self):
return self._m[:self.__dimension__,-1]
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
##############################################
def _check_type(self):
matrix_type = Transformation2D.check_matrix_type(self.matrix_part.flat)
# Fixme: translation etc. !!!
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
transformation._type = TransformationType.Rotation
return transformation
##############################################
@classmethod
def Scale(cls, x_scale, y_scale):
# Fixme: others, use *= ? (comment means ???)
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
transformation._type = cls.type_for_scale(x_scale, y_scale)
return transformation
##############################################
@classmethod
def Screen(cls, y_height):
transformation = cls.Identity()
# Fixme: better ?
transformation.matrix_part[...] = Transformation2D.YReflection().array
transformation.translation_part[...] = Vector2D(0, y_height).v[...]
transformation._type = TransformationType.Generic
return transformation
#######################################
def __mul__(self, obj):
if isinstance(obj, HomogeneousVector2D):
array = np.matmul(self._m, obj.v)
return obj.__class__(array)
elif isinstance(obj, Vector2D):
array = np.matmul(self._m, HomogeneousVector2D(obj).v)
# return HomogeneousVector2D(array).to_vector()
return Vector2D(array[:2])
else:
return super(AffineTransformation, self).__mul__(obj)
####################################################################################################
# The matrix to rotate an angle θ about the axis defined by unit vector (l, m, n) is
# l*l*(1-c) + c , m*l*(1-c) - n*s , n*l*(1-c) + m*s
# l*m*(1-c) + n*s , m*m*(1-c) + c , n*m*(1-c) - l*s
# l*n*(1-c) - m*s , m*n*(1-c) + l*s , n*n*(1-c) + c