Newer
Older
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
#
# 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 <http://www.gnu.org/licenses/>.
#
####################################################################################################
Example of usage::
v = Vector2D(10, 20)
v = Vector2D((10, 20))
v = Vector2D([10, 20])
v = Vector2D(iterable)
v = Vector2D(v)
v.x
v.y
# array interface
v[0], v[1]
iter(v)
-v
v + v
v += v
v - v
v -= v
v * 2
2 * v
v *= 2
v / 2
v /= 2
"""
####################################################################################################
__all__ = [
'Vector2D',
'NormalisedVector2D',
'HomogeneousVector2D',
]
####################################################################################################
import math
import numpy as np
from IntervalArithmetic import Interval2D, IntervalInt2D
from Patro.Common.Math.Functions import sign, trignometric_clamp #, is_in_trignometric_range
from .Primitive import Primitive, Primitive2DMixin
####################################################################################################
class Vector2DBase(Primitive, Primitive2DMixin):
def __init__(self, *args):
array = self._check_arguments(args)
# call __getitem__ once
self._v = np.array(array[:2], dtype=self.__data_type__) # Numpy implementation
##############################################
def _check_arguments(self, args):
size = len(args)
if size == 1:
array = args[0]
elif size == 2:
array = args
else:
raise ValueError("More than 2 arguments where given")
if not (np.iterable(array) and len(array) == 2):
raise ValueError("Argument must be iterable and of length 2")
return array
##############################################
def clone(self):
return self.__class__(self)
##############################################
@property
def v(self):
return self._v
# @v.setter
# def v(self, value):
# self._v = value
return self.__data_type__(self._v[0])
return self.__data_type__(self._v[1])
def x(self, x):
self._v[0] = x
def y(self, y):
self._v[1] = y
def clone(self):
return self.__class__(self._v)
return self.__class__.__name__ + str(self.v)
return bool(self._v.any())
def __len__(self):
return 2
return iter(self._v)
return self._v[a_slice]
def __setitem__(self, index, value):
self._v[index] = value
def __eq__(v1, v2):
""" self == other """
return np.array_equal(v1.v, v2.v)
"""Return a new vector equal to the addition of self and other"""
return self.__class__(self._v + other.v)
self._v += other.v
return self.__class__(self._v - other.v)
"""Return a new vector equal to the subtraction of self and other"""
self._v -= other.v
def __pos__(self):
""" Return a new vector equal to self """
return self.__class__(self._v)
def __neg__(self):
"""Return a new vector equal to the negation of self"""
return self.__class__(-self._v)
def __abs__(self):
"""Return a new vector equal to abs of self"""
return self.__class__(np.abs(self._v))
##############################################
def to_int_list(self):
return [int(x) for x in self._v]
####################################################################################################
class Vector2DInt(Vector2DBase):
__data_type__ = np.int
##############################################
def bounding_box(self):
x, y = self.x, self.y
return IntervalInt2D((x, x) , (y, y))
####################################################################################################
class Vector2DFloatBase(Vector2DBase):
__data_type__ = np.float
##############################################
def bounding_box(self):
x, y = self.x, self.y
return Interval2D((x, x) , (y, y))
def almost_equal(v1, v2, rtol=1e-05, atol=1e-08, equal_nan=False):
return np.allclose(v1, v2, rtol, atol, equal_nan)
"""Return the square of the magnitude of the vector"""
return np.dot(self._v, self._v)
# Note: To avoid float overflow use
# abs(x) * sqrt(1 + (y/x)**2) if x > y
#
# 2 | 1
# - + -
# 4 | 3
#
# | 1 | 2 | 3 | 4 |
# x | + | - | + | - |
# y | + | + | - | - |
# tan | + | - | - | + |
# atan | + | - | - | + |
# theta | atan | atan + pi | atan | atan - pi |
#
if not bool(self):
raise NameError("Null Vector")
if self.x == 0:
return math.copysign(90, self.y)
elif self.y == 0:
return 0 if self.x >= 0 else 180
else:
orientation = math.degrees(math.atan(self.tan))
if self.x < 0:
if self.y > 0:
orientation += 180
else:
orientation -= 180
return orientation
"""Return a new vector equal to self rotated of angle degree in the counter clockwise direction
"""
radians = math.radians(angle)
c = math.cos(radians)
s = math.sin(radians)
# Fixme: np matrice
xp = c * self._v[0] -s * self._v[1]
yp = s * self._v[0] +c * self._v[1]
return self.__class__((xp, yp))
"""Return a new vector equal to self rotated of 90 degree in the counter clockwise direction
xp = -self._v[1]
yp = self._v[0]
return self.__class__((xp, yp))
"""Return a new vector equal to self rotated of 90 degree in the clockwise direction
xp = self._v[1]
yp = -self._v[0]
return self.__class__((xp, yp))
@property
def permute(self):
"""Return a new vector where x and y are permuted.
"""
xp = self._v[1]
yp = self._v[0]
return self.__class__((xp, yp))
##############################################
"""Return a new vector equal to self rotated of 180 degree
xp = -self._v[0]
yp = -self._v[1]
return self.__class__((xp, yp))
# RuntimeWarning: divide by zero encountered in double_scalars
return self.y / self.x
"""Return the dot product of self with other"""
return float(np.dot(self._v, other.v))
def cross(self, other):
"""Return the cross product of self with other"""
return float(np.cross(self._v, other.v))
##############################################
# perp dot product
# perp = (-y1, x1)
# perp dot = -y1*x2 + x1*y2 = x1*y2 - x2*y1
perp_dot = cross
##############################################
def is_parallel(self, other, cross=False):
cross = self.cross(other)
test = round(cross, 7) == 0
if cross:
return test, cross
else:
return test
return round(self.dot(other), 7) == 0
"""Return the cosinus of self with direction"""
cos = direction.dot(self) / (direction.magnitude * self.magnitude)
def projection_on(self, direction):
"""Return the projection of self on direction"""
return direction.dot(self) / direction.magnitude
sin = direction.cross(self) / (direction.magnitude * self.magnitude)
def deviation_with(self, direction):
return direction.cross(self) / direction.magnitude
def angle_with(self, direction):
angle = math.acos(self.cos_with(direction))
angle_sign = sign(self.sin_with(direction))
return angle_sign * math.degrees(angle)
orientation_with = angle_with
####################################################################################################
class Vector2D(Vector2DFloatBase):
@staticmethod
def from_angle(angle):
"""Create the unitary vector (cos(angle), sin(angle)). *angle* is in degree."""
return Vector2D((math.cos(rad), math.sin(rad))) # Fixme: classmethod
@staticmethod
def from_polar(radius, angle):
"""Create the polar vector (radius*cos(angle), radius*sin(angle)). *angle* is in degree."""
return Vector2D.from_angle(angle) * radius # Fixme: classmethod
##############################################
def from_ellipse(x_radius, y_radius, angle):
"""Create the vector (x_radius*cos(angle), y_radius*sin(angle)). *angle* is in degree."""
angle = math.radians(angle)
x = x_radius * cos(angle)
y = y_radius * sin(angle)
return Vector2D(x, y) # Fixme: classmethod
##############################################
@staticmethod
"""Return the middle point."""
return Vector2D(p0 + p1) * .5 # Fixme: classmethod
"""Return a new vector equal to the self scaled by scale"""
return self.__class__(scale * self._v) # Fixme: Vector2D ?
def __rmul__(self, scale):
"""Return a new vector equal to the self scaled by scale"""
return self.__mul__(scale)
##############################################
self._v *= scale
"""Return a new vector equal to the self dvivided by scale"""
return self.__class__(self._v / scale)
self._v /= scale
def divide(self, scale_x, scale_y):
"""Scale self by 1/scale"""
obj = self.clone()
obj._v /= np.array((scale_x, scale_y))
return obj
##############################################
return NormalisedVector2D(self._v / self.magnitude)
return Vector2DInt(np.rint(self._v))
####################################################################################################
class NormalisedVector2D(Vector2DFloatBase):
def __init__(self, *args):
super(NormalisedVector2D, self).__init__(*args)
#! raise ValueError("Magnitude != 1")
# if not (is_in_trignometric_range(self.x) and
# is_in_trignometric_range(self.y)):
# raise ValueError("Values must be in trignometric range")
def __mul__(self, scale):
""" Return a new vector equal to the self scaled by scale """
return self.__class__(scale * self._v) # Fixme: Vector2D ?
##############################################
def __rmul__(self, scale):
""" Return a new vector equal to the self scaled by scale """
return self.__mul__(scale)
####################################################################################################
class HomogeneousVector2D(Vector2D):
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
def __init__(self, vector):
# self._v = np.ones((3), dtype=self.__data_type__)
# self._v[:2] = vector.v[:2]
self._v = np.array(vector[:2]) # to keep compatibility
self._w = 1
##############################################
@property
def v(self):
return np.array(((self.x), (self.y), (self._w)), dtype=self.__data_type__)
# @v.setter
# def v(self, value):
# self._v = value
@property
def w(self):
return self._w
@w.setter
def w(self, value):
self._w = value
##############################################
def to_vector(self):
return Vector2D(self._v)