Skip to content
Primitive.py 15.3 KiB
Newer Older
####################################################################################################
#
Fabrice Salvaire's avatar
Fabrice Salvaire committed
# 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 <http://www.gnu.org/licenses/>.
#
####################################################################################################

Fabrice Salvaire's avatar
Fabrice Salvaire committed
"""Module to implement base classes for primitives.

"""

# Fixme:
#  length, interpolate path
#  area

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

__all__ = [
    'Primitive',
    'Primitive2DMixin',
    'Primitive1P',
    'Primitive2P',
    'Primitive3P',
    'Primitive4P',
]

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

from collections import abc as collections
import numpy as np

from .BoundingBox import bounding_box_from_points
# Fixme: circular import
# from .Transformation import Transformation2D
####################################################################################################

class Primitive:

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    """Base class for geometric primitive"""

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    __vector_cls__ = None

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def dimension(self):
        """Dimension in [2, 3] for 2D / 3D primitive"""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        raise NotImplementedError

    ##############################################
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def is_infinite(self):
        """True if the primitive has infinite extend like a line"""
        return False

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

    @property
    def is_closed(self):
        """True if the primitive is a closed path."""
        return False

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def number_of_points(self):
        """Number of points which define the primitive."""
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def __len__(self):
        return self.number_of_points

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

    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def is_reversible(self):
        """True if the order of the points is reversible"""
        # Fixme: True if number_of_points > 1 ???
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        return False

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

    # Fixme: part of the API imply points
    #  it is not true for Path2D

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def points(self):
        raise NotImplementedError

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

    # @points.setter
    # def points(self):
    #     raise NotImplementedError

    def _set_points(self, points):
        raise NotImplementedError

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

    def __repr__(self):
        return self.__class__.__name__ + str([str(p) for p in self.points])

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

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

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

    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def bounding_box(self):
        """Bounding box of the primitive.

        Return None if primitive is infinite.
        """
        # Fixme: cache
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        if self.is_infinite:
            return None
        else:
            return bounding_box_from_points(self.points)

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def reverse(self):
        return self

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

    def transform(self, transformation, clone=False):
        """Apply a transformation to the primitive.
        If *clone* is set then the primitive is cloned.

        """
        obj = self.clone() if clone else self
        if not transformation.is_identity:
            obj.apply_transformation(transformation)
        return obj

    ##############################################
    def apply_transformation(self, transformation):
        """Apply a transformation to the primitive.

        If *clone* is set then the primitive is cloned.

        """
        # for point in self.points:
        #     point *= transformation # don't work
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        self._set_points([transformation*p for p in self.points])

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

    def mirror(self, clone=False):
        from .Transformation import Transformation2D
        return self.transform(Transformation2D.Parity(), clone)

    def x_mirror(self, clone=False):
        from .Transformation import Transformation2D
        return self.transform(Transformation2D.XReflection(), clone)

    def y_mirror(self, clone=False):
        from .Transformation import Transformation2D
        return self.transform(Transformation2D.YReflection(), clone)

    def rotate(self, angle, clone=False):
        from .Transformation import Transformation2D
        return self.transform(Transformation2D.Rotation(angle), clone)

    def scale(self, x_factor, y_factor=None, clone=False):
        from .Transformation import Transformation2D
        return self.transform(Transformation2D.Scale(x_factor, y_factor), clone)

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

    def point_array(self):
        r"""Return the geometry matrix as a Numpy array.

        .. math::
           \mathrm{Geometry\ Matrix} =
           \begin{bmatrix}
           x_0  & x_1 & \ldots & x_{n-1} \\
           y_0  & y_1 & \ldots & y_{n-1}
           \end{bmatrix}

        """
        # Fixme: geometry_matrix vs point_array
        # Fixme: cache ??? but point set and init
        # if self._point_array is None:
        #     self._point_array = np.array(list(self.points)).transpose()
        # return self._point_array
        return np.array(list(self.points)).transpose()

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

    def is_point_close(self, other):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        # Fixme: verus is_closed
        #  is_similar
        return np.allclose(self.point_array, other.point_array)
####################################################################################################

class Primitive2DMixin:
    __vector_cls__ = None # Fixme: due to import, done in module's __init__.py
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def dimension(self):
        return 2
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

class ClosedPrimitiveMixin:

    # Fixme: should be reversible

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

    @property
    def is_closed(self):
        """True if the primitive is a closed path."""
        return True

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

    @property
    def closed_points(self):
        points = list(self.points)
        points.append(self.start_point)
        return points

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

    @property
    def closed_point_array(self):
        r"""Return the geometry matrix as a Numpy array for a closed primitive.

        .. math::
           \mathrm{Geometry\ Matrix} =
           \begin{bmatrix}
           x_0  & x_1 & \ldots & x_{n-1} & x_0 \\
           y_0  & y_1 & \ldots & y_{n-1} & y_0
           \end{bmatrix}

        """
        # Fixme: place, func for closed_point
        # Fixme: cache ???
        return np.array(self.closed_points).transpose()

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
class ReversiblePrimitiveMixin:
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    @property
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def is_reversible(self):
        True
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    @property
    def reversed_points(self):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        raise NotImplementedError
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        # return reversed(list(self.points))

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

    def reverse(self):
        return self.__class__(*self.reversed_points)
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    @property
    def start_point(self):
        raise NotImplementedError

    @property
    def end_point(self):
        raise NotImplementedError

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

class Primitive1P(Primitive):

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

    def __init__(self, p0):
        self.p0 = p0

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

    @property
    def number_of_points(self):
        return 1

    @property
    def p0(self):
        return self._p0

    @p0.setter
    def p0(self, value):
        self._p0 = self.__vector_cls__(value)

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

    @property
    def points(self):
        return iter(self._p0) # Fixme: efficiency ???

    @property
    def reversed_points(self):
        return self.points

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

    def _set_points(self, points):
        self._p0 = points

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

class Primitive2P(Primitive1P, ReversiblePrimitiveMixin):

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

    def __init__(self, p0, p1):
        # We don't call super().__init__(p0) for speed
        self.p0 = p0
        self.p1 = p1

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

    # Redundant code ... until we don't use self._points = []

    @property
    def number_of_points(self):
        return 2

    @property
    def p1(self):
        return self._p1

    @p1.setter
    def p1(self, value):
        self._p1 = self.__vector_cls__(value)

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

    @property
    def start_point(self):
        return self._p0

    @property
    def end_point(self):
        return self._p1

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

    @property
    def points(self):
        return iter((self._p0, self._p1))
Fabrice Salvaire's avatar
Fabrice Salvaire committed

    @property
    def reversed_points(self):
        return iter((self._p1, self._p0))
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    def _set_points(self, points):
        self._p0, self._p1 = points

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

    def interpolate(self, t):
        """Return the linear interpolate of two points."""
        return self._p0 * (1 - t) + self._p1 * t

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

class Primitive3P(Primitive2P):

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

    def __init__(self, p0, p1, p2):
        self.p0 = p0
        self.p1 = p1
        self.p2 = p2

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

    @property
    def number_of_points(self):
        return 3

    @property
    def p2(self):
        return self._p2

    @p2.setter
    def p2(self, value):
        self._p2 = self.__vector_cls__(value)

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

    @property
    def end_point(self):
        return self._p2

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

    @property
    def points(self):
        return iter((self._p0, self._p1, self._p2))
Fabrice Salvaire's avatar
Fabrice Salvaire committed

    @property
    def reversed_points(self):
        # Fixme: share code ???
        return iter((self._p2, self._p1, self._p0))
    def iter_from_second_point(self):
        # Fixme: share code ???
        return iter(self._p1, self._p2)

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

    def _set_points(self, points):
        self._p0, self._p1, self._p2 = points

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

class Primitive4P(Primitive3P):
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    def __init__(self, p0, p1, p2, p3):
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        self.p0 = p0
        self.p1 = p1
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        self.p2 = p2
        self.p3 = p3

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

    @property
    def number_of_points(self):
        return 4

    @property
    def p3(self):
        return self._p3

    @p3.setter
    def p3(self, value):
        self._p3 = self.__vector_cls__(value)

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

    @property
    def end_point(self):
        return self._p3

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

    @property
    def points(self):
        return iter((self._p0, self._p1, self._p2, self._p3))
Fabrice Salvaire's avatar
Fabrice Salvaire committed

    @property
    def reversed_points(self):
        return iter((self._p3, self._p2, self._p1, self._p0))
    def iter_from_second_point(self):
        return iter(self._p1, self._p2, self._p3)

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

    def _set_points(self, points):
        self._p0, self._p1, self._p2, self._p3 = points

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

class PrimitiveNP(Primitive, ReversiblePrimitiveMixin):
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    @staticmethod
    def handle_points(points):
        if len(points) == 1 and isinstance(points[0], collections.Iterable):
            points = points[0]
        return points

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

    def __init__(self, *points):
        points = self.handle_points(points)
Fabrice Salvaire's avatar
Fabrice Salvaire committed
        self._points = [self.__vector_cls__(p) for p in points]
        self._point_array = None
Fabrice Salvaire's avatar
Fabrice Salvaire committed

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

    @property
    def number_of_points(self):
        return len(self._points)

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

    @property
    def start_point(self):
        return self._points[0]

    @property
    def end_point(self):
        return self._points[-1]

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

    @property
    def points(self):
        return iter(self._points)

    @property
    def reversed_points(self):
        return reversed(self._points)

    def iter_from_second_point(self):
        return iter(self._points[1:])

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

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def _set_points(self, points):
        self._points = points
        self._point_array = None
Fabrice Salvaire's avatar
Fabrice Salvaire committed
    ##############################################

Fabrice Salvaire's avatar
Fabrice Salvaire committed
    def __getitem__(self, _slice):
        return self._points[_slice]

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

    def iter_on_nuplets(self, size):

        if size > self.number_of_points:
            raise ValueError('size {} > number of points {}'.format(size, self.number_of_points))

        for i in range(self.number_of_points - size +1):
            yield self._points[i:i+size]

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

class PolygonMixin:

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

    def to_polygon(self):
        from .Polygon import Polygon2D
        return Polygon2D(self.points)

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

class PathMixin:

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

    def to_path(self):

        from .Path import Path2D

        path = Path2D(self.start_point)
        for point in self.iter_from_second_point():
            path.line_to(point)
        if self.is_closed:
            path.close()

        return path