Skip to content
Vector.py 17 KiB
Newer Older
####################################################################################################
#
Fabrice Salvaire's avatar
Fabrice Salvaire committed
# Patro - A Python library to make patterns for fashion design
Fabrice Salvaire's avatar
Fabrice Salvaire committed
# 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 <http://www.gnu.org/licenses/>.
#
####################################################################################################

Fabrice Salvaire's avatar
Fabrice Salvaire committed
"""Module to implement vector.

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
"""

####################################################################################################

__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):

    __data_type__ = None

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __init__(self, *args):

        array = self._check_arguments(args)

        # call __getitem__ once
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def clone(self):
        return self.__class__(self)

    ##############################################
    @property
    def v(self):
        return self._v

    # @v.setter
    # def v(self, value):
    #     self._v = value

    @property
    def x(self):
        return self.__data_type__(self._v[0])

    @property
    def y(self):
        return self.__data_type__(self._v[1])
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
        """ Return a copy of self """
        return self.__class__(self._v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __repr__(self):
        return self.__class__.__name__ + str(self.v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __nonzero__(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __len__(self):
        return 2

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __iter__(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __getitem__(self, a_slice):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __setitem__(self, index, value):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __eq__(v1, v2):
        """ self == other """
        return np.array_equal(v1.v, v2.v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __add__(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to the addition of self and other"""
        return self.__class__(self._v + other.v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __iadd__(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Add other to self"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __sub__(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector"""
        return self.__class__(self._v - other.v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __isub__(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to the subtraction of self and other"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __pos__(self):
        """ Return a new vector equal to self """
        return self.__class__(self._v)

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to the negation of self"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """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

    ##############################################

    @property
    def bounding_box(self):
        x, y = self.x, self.y
        return IntervalInt2D((x, x) , (y, y))

####################################################################################################

class Vector2DFloatBase(Vector2DBase):

    __data_type__ = np.float

    ##############################################

    @property
    def bounding_box(self):
        x, y = self.x, self.y
        return Interval2D((x, x) , (y, y))

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    def almost_equal(v1, v2, rtol=1e-05, atol=1e-08, equal_nan=False):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """self ~= other"""
        return np.allclose(v1, v2, rtol, atol, equal_nan)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
    def magnitude_square(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the square of the magnitude of the vector"""
        return np.dot(self._v, self._v)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
    def magnitude(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the magnitude of the vector"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        # Note: To avoid float overflow use
        #   abs(x) * sqrt(1 + (y/x)**2)  if x > y
        return math.sqrt(self.magnitude_square)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
    def orientation(self):

Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the orientation in degree"""

        #
        # 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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def rotate(self, angle, counter_clockwise=True):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to self rotated of angle degree in the counter clockwise direction

        """

        radians = math.radians(angle)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        if not counter_clockwise:
            radians = -radians
        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))

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def normal(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to self rotated of 90 degree in the counter clockwise direction


        return self.__class__((xp, yp))

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def anti_normal(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to self rotated of 90 degree in the clockwise direction


        return self.__class__((xp, yp))

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @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))

    ##############################################

    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def parity(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to self rotated of 180 degree

        xp = -self._v[0]
        yp = -self._v[1]

        return self.__class__((xp, yp))

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
    def tan(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the tangent"""
        # RuntimeWarning: divide by zero encountered in double_scalars
        return self.y / self.x

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @property
    def inverse_tan(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the inverse tangent"""
        return self.x / self.y

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def dot(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the dot product of self with other"""
        return float(np.dot(self._v, other.v))
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    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, return_cross=False):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Self is parallel with other"""

        cross = self.cross(other)
        test = round(cross, 7) == 0
            return test, cross
        else:
            return test
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def is_orthogonal(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Self is orthogonal with other"""
        return round(self.dot(other), 7) == 0

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def cos_with(self, direction):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the cosinus of self with direction"""
        cos = direction.dot(self) / (direction.magnitude * self.magnitude)
        return trignometric_clamp(cos)

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def projection_on(self, direction):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the projection of self on direction"""
        return direction.dot(self) / direction.magnitude
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def sin_with(self, direction):

Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the sinus of self with other"""

        # turn from direction to self
        sin = direction.cross(self) / (direction.magnitude * self.magnitude)

        return trignometric_clamp(sin)

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def deviation_with(self, direction):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the deviation of self with other"""
        return direction.cross(self) / direction.magnitude
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    def angle_with(self, direction):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the angle of self on direction"""

        angle = math.acos(self.cos_with(direction))
        angle_sign = sign(self.sin_with(direction))

        return angle_sign * math.degrees(angle)

####################################################################################################

class Vector2D(Vector2DFloatBase):

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    """2D Vector"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    @staticmethod
    def from_angle(angle):

        """Create the unitary vector (cos(angle), sin(angle)).  *angle* is in degree."""

        rad = math.radians(angle)

Fabrice Salvaire's avatar
Fabrice Salvaire committed
        return Vector2D((math.cos(rad), math.sin(rad))) # Fixme: classmethod
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    @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

    ##############################################

    @staticmethod
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def from_ellipse(radius_x, radius_y, angle):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Create the vector (radius_x*cos(angle), radius_y*sin(angle)).  *angle* is in degree."""

        angle = math.radians(angle)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        x = radius_x * cos(angle)
        y = radius_y * sin(angle)

        return Vector2D(x, y) # Fixme: classmethod

    ##############################################

    @staticmethod
    def middle(p0, p1):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return the middle point."""
        return Vector2D(p0 + p1) * .5 # Fixme: classmethod
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __mul__(self, scale):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to the self scaled by scale"""
        return self.__class__(scale * self._v) # Fixme: Vector2D ?
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
    def __rmul__(self, scale):
        """Return a new vector equal to the self scaled by scale"""
        return self.__mul__(scale)

    ##############################################

    def __imul__(self, scale):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Scale self by scale"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __truediv__(self, scale):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a new vector equal to the self dvivided by scale"""
        return self.__class__(self._v / scale)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __itruediv__(self, scale):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Scale self by 1/scale"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def scale(self, scale_x, scale_y):
        """Scale self by scale"""
        obj = self.clone()
        obj._v *= np.array((scale_x, scale_y))
        return obj

    ##############################################

    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

    ##############################################

    def normalise(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Normalise the vector"""
        self._v /= self.magnitude
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        return self
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def to_normalised(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        """Return a normalised vector"""
        return NormalisedVector2D(self._v / self.magnitude)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def rint(self):
        return Vector2DInt(np.rint(self._v))

####################################################################################################

class NormalisedVector2D(Vector2DFloatBase):

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    """2D Normalised Vector"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __init__(self, *args):

        super(NormalisedVector2D, self).__init__(*args)

        #! if self.magnitude != 1.:
        #!     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")

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    def __mul__(self, scale):
        """ Return a new vector equal to the self scaled by scale """
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        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):

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    """2D Homogeneous Coordinate Vector"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

    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)