def test_cross_product():
    """Check cross product of two Vectors."""
    Ω = Point(0, 0, 'Ω')
    A = Point(1, 1, 'A')
    with pytest.raises(TypeError) as excinfo:
        Vector(0, 1).cross(Bipoint(Ω, A))
    assert str(excinfo.value) == 'Can only calculate the cross product of a '\
        'Vector by another Vector. '\
        'Found Bipoint(Point Ω(0, 0), Point A(1, 1)) instead.'
    assert Vector(1, 0).cross(Vector(0, 1)) == Vector(0, 0, 1)
    assert Vector(1, 0, 0).cross(Vector(0, 1, 0)) == Vector(0, 0, 1)
    assert Vector(3, 4, 5).cross(Vector(2, 5, 6)) == Vector(-1, -8, 7)
示例#2
0
 def slope360(self):
     """Slope of the pair of Points, from 0° to 360°."""
     try:
         return Vector(self).slope360
     except ZeroVector:
         msg = 'Cannot calculate the slope of a zero-length {}.'\
             .format(type(self).__name__)
         raise ZERO_OBJECTS_ERRORS[type(self).__name__](msg)
def test_dot_product():
    """Check dot product of two Vectors."""
    Ω = Point(0, 0, 'Ω')
    A = Point(1, 1, 'A')
    with pytest.raises(TypeError) as excinfo:
        Vector(0, 1).dot(Bipoint(Ω, A))
    assert str(excinfo.value) == 'Can only calculate the dot product of a '\
        'Vector by another Vector. '\
        'Found Bipoint(Point Ω(0, 0), Point A(1, 1)) instead.'
    u = Vector(1, 1)
    v = Vector(3, -7)
    assert u.dot(v) == -4
    u = Vector(2, 5, -7)
    v = Vector(3, -7, 2)
    assert u.dot(v) == -43
def test_instanciation_errors():
    """Check Vector's instanciation exceptions."""
    Point.reset_names()
    with pytest.raises(TypeError) as excinfo:
        Vector()
    assert str(excinfo.value) == 'Vector() takes one, two or three arguments '\
        '(0 given)'
    with pytest.raises(TypeError) as excinfo:
        Vector(Point(0, 0))
    assert str(excinfo.value) == 'a Vector can be created from one Bipoint, '\
        'found Point A(0, 0) instead.'
    Point.reset_names()
    with pytest.raises(TypeError) as excinfo:
        Vector(Point(0, 0), 4)
    assert str(excinfo.value) == 'a Vector can be created from two '\
        'arguments, either two Points or two numbers. Found Point A(0, 0) '\
        'and 4 instead.'
    with pytest.raises(ZeroVector) as excinfo:
        Vector(Point(1, 1), Point(1, 1), allow_zero_length=False)
    assert str(excinfo.value) == 'Explicitly disallowed creation of a '\
        'zero-length Vector.'
def test_normalized():
    """Check unit Vector creation."""
    assert Vector(3, 4).normalized() == Vector(0.6, 0.8)
    v = Vector(3, 4, 0).normalized()
    assert v.three_dimensional
    assert v == Vector(0.6, 0.8, 0)
    assert Vector(12, 15, 16).normalized() == Vector(0.48, 0.6, 0.64)
def test_addition():
    """Check Vectors' additions."""
    Ω = Point(0, 0, 'Ω')
    A = Point(1, 1, 'A')
    with pytest.raises(TypeError) as excinfo:
        Vector(0, 1) + Bipoint(Ω, A)
    assert str(excinfo.value) == 'Can only add a Vector to another Vector. '\
        'Found Bipoint(Point Ω(0, 0), Point A(1, 1)) instead.'
    u = Vector(1, 1)
    v = Vector(3, -7)
    assert u + v == Vector(4, -6)
    v = Vector(3, -7, 2)
    assert u + v == Vector(4, -6, 2)
    u = Vector(2, 5, -7)
    assert u + v == Vector(5, -2, -5)
def test_angle_measure():
    """Check angle measure between two Vectors."""
    Ω = Point(0, 0)
    pointI = Point(1, 0)
    A = Point(1, 1)
    i = Vector(Ω, pointI)
    a = Vector(Ω, A)
    assert i.angle_measure(a) == 45
    assert a.angle_measure(i) == 315
def test_instanciation():
    """Check Vector's instanciation."""
    u = Vector(Bipoint(Point(0, 0), Point(2, 5)))
    assert not u.three_dimensional
    u = Vector(Bipoint(Point(0, 0, 0), Point(2, 5)))
    assert u.three_dimensional
    v = Vector(Point(0, 0), Point(2, 5))
    assert not v.three_dimensional
    v = Vector(Point(0, 0, 0), Point(2, 5))
    assert v.three_dimensional
    w = Vector(2, 5)
    assert not w.three_dimensional
    assert w.z == 0
    assert w.x == 2
    assert w.y == 5
    assert w.coordinates == (2, 5, 0)
    w = Vector(2, 5, 0)
    assert w.three_dimensional
示例#9
0
    def __init__(self,
                 *vertices,
                 name=None,
                 draw_vertices=False,
                 label_vertices=True,
                 thickness='thick',
                 color=None,
                 rotation_angle=0,
                 winding=None,
                 do_cycle=True,
                 sloped_sides_labels=True):
        r"""
        Initialize Polygon

        :param vertices: the vertices of the Polygon
        :type vertices: a list of at least three Points
        :param name: the name of the Polygon, like ABCDE for a pentagon. Can
        be either None (the names of the provided Points will be kept), or a
        string of the letters to use to rename the provided Points. Only
        single letters are supported as Points' names so far (at Polygon's
        creation). See issue #3.
        :type name: None or str
        :param draw_vertices: whether to actually draw, or not, the vertices
        :type draw_vertices: bool
        :param label_vertices: whether to label, or not, the vertices
        :type label_vertices: bool
        :param thickness: the thickness of the Polygon's sides
        :type thickness: str
        :param color: the color of the Polygon's sides
        :type color: str
        :param rotate: the angle of rotation around isobarycenter
        :type rotate: int
        :param winding: force the winding to be either 'clockwise' or
        'anticlockwise'. If left to None (default), doesn't force anything,
        the winding will be either forced by the value of
        config.DEFAULT_POLYGON_WINDING, or if it is None too, then the
        winding will be deduced from the given vertices' order.
        :type winding: None or a str ('clockwise' or 'anticlockwise')
        """
        self.thickness = thickness
        self.color = color
        self.do_cycle = do_cycle
        self.draw_vertices = draw_vertices
        self.label_vertices = label_vertices
        if len(vertices) <= 2:
            raise ValueError('At least three Points are required to be able '
                             'to build a Polygon. Got only {} positional '
                             'arguments, though.'.format(len(vertices)))
        if any([not isinstance(v, Point) for v in vertices]):
            for i, v in enumerate(vertices):
                if not isinstance(v, Point):
                    raise TypeError('Only Points must be provided in order to '
                                    'build a Polygon. Got a {} as positional '
                                    'argument #{}.'.format(type(v), i))
        if not is_number(rotation_angle):
            raise TypeError('Expected a number as rotation angle, got a {} '
                            'instead.'.format(type(rotation_angle)))
        if name is not None:
            if len(name) != len(vertices):
                raise ValueError('The number of provided vertices ({}) does '
                                 'not match the number of Points\' names '
                                 '({}).'.format(len(vertices), len(name)))

        if (winding is None and config.polygons.DEFAULT_WINDING is not None):
            winding = config.polygons.DEFAULT_WINDING

        if winding is not None:
            check_winding(winding)

        self._reverted_winding = False

        if shoelace_formula(*vertices) < 0:
            if winding == 'anticlockwise':
                vertices = vertices[::-1]
                self._reverted_winding = True
                self.winding = 'anticlockwise'
            else:
                self.winding = 'clockwise'
        else:
            if winding == 'clockwise':
                vertices = vertices[::-1]
                self._reverted_winding = True
                self.winding = 'clockwise'
            else:
                self.winding = 'anticlockwise'

        self._vertices = []
        self._three_dimensional = False
        for i, v in enumerate(vertices):
            if name is None:
                vname = v.name
            else:
                vname = name[i]
            if v.name == v.label:
                lbl = 'default'
            else:
                lbl = v.label
            if v.three_dimensional:
                self._three_dimensional = True
                zval = v.z
            else:
                zval = 'undefined'
            self._vertices.append(
                Point(v.x,
                      v.y,
                      z=zval,
                      name=vname,
                      shape=v.shape,
                      label=lbl,
                      color=v.color,
                      shape_scale=v.shape_scale))
        if rotation_angle:
            center = self.isobarycenter()
            for i in range(len(self._vertices)):
                self._vertices[i] = self._vertices[i].rotate(
                    center=center, angle=rotation_angle, rename='keep_name')

        self._sides = []
        shifted_vertices = deepcopy(self._vertices)
        shifted_vertices += [shifted_vertices.pop(0)]
        for (v0, v1) in zip(self._vertices, shifted_vertices):
            self._sides += [
                LineSegment(v0,
                            v1,
                            label_winding=self.winding,
                            locked_label=True)
            ]
        self._angles = []
        left_shifted_vertices = deepcopy(self._vertices)
        left_shifted_vertices = \
            [left_shifted_vertices.pop(-1)] + left_shifted_vertices
        for (v0, v1, v2) in zip(left_shifted_vertices, self._vertices,
                                shifted_vertices):
            self._angles += [Angle(v2, v1, v0)]
        for i in range(len(self._vertices)):
            u = Vector(self._vertices[i], left_shifted_vertices[i])
            v = Vector(self._vertices[i], shifted_vertices[i])
            if self.winding == 'clockwise':
                u, v = v, u
            self._vertices[i].label_position = \
                tikz_approx_position(u.bisector(v).slope360)

        if len(self._sides) in POLYGONS_TYPES:
            self._type = POLYGONS_TYPES[len(self._sides)]
        else:
            self._type = \
                '{n}-sided Polygon'.format(n=str(len(self._sides)))

        self.sloped_sides_labels = sloped_sides_labels

        if (self._reverted_winding
                and config.polygons.ENABLE_MISMATCH_WINDING_WARNING):
            warnings.warn('Changed the order of Points to comply with forced '
                          'winding ({}) for {}.'.format(winding, repr(self)))
    def __init__(self,
                 object3D=None,
                 k=None,
                 α=None,
                 direction=None,
                 thickness='thick',
                 color=None,
                 draw_vertices=False,
                 label_vertices=False):
        r"""
        Initialize ObliqueProjection.

        :param object3D: the object to project.
        :type object3D: Polyhedron
        :param k: the ratio of the oblique projection. Defaults to
        config.oblique_projection.RATIO
        :type k: number
        :param α: the angle between the receding Z-axis, and X-axis.
        :type α: number
        :param direction: 'top-left', 'top-right', 'bottom-left' or
        'bottom-right'
        :type direction: str
        """
        self.draw_vertices = draw_vertices
        self.label_vertices = label_vertices
        if k is None:
            k = config.oblique_projection.RATIO
        if not is_number(k):
            raise TypeError(
                'Ratio k must be a number. Found {} instead.'.format(repr(k)))
        if α is None:
            α = config.oblique_projection.RECEDING_AXIS_ANGLE
        if direction is None:
            direction = config.oblique_projection.DIRECTION
        if not is_number(α):
            raise TypeError(
                'Angle α must be a number. Found {} instead.'.format(repr(α)))
        if not isinstance(object3D, Polyhedron):
            raise TypeError(
                'object3D must be a Polyhedron, found {} instead.'.format(
                    repr(object3D)))
        check_direction(direction)
        self._direction = direction
        self._object3D_name = type(object3D).__name__

        # Setup the edges' labels
        if object3D.labels is not None:
            edges_to_label = object3D.edges_to_label[
                'oblique_projection:{}'.format(direction)]
            w_coord, d_coord, h_coord = edges_to_label
            object3D.faces[w_coord[0]].sides[w_coord[1]].unlock_label()
            object3D.faces[w_coord[0]].sides[w_coord[1]].label_winding = \
                w_coord[2]
            object3D.faces[w_coord[0]].sides[w_coord[1]].label = \
                object3D.labels[0]
            object3D.faces[w_coord[0]].sides[w_coord[1]].lock_label()
            object3D.faces[d_coord[0]].sides[d_coord[1]].unlock_label()
            object3D.faces[d_coord[0]].sides[d_coord[1]].label_winding = \
                d_coord[2]
            object3D.faces[d_coord[0]].sides[d_coord[1]].label = \
                object3D.labels[1]
            object3D.faces[d_coord[0]].sides[d_coord[1]].lock_label()
            object3D.faces[h_coord[0]].sides[h_coord[1]].unlock_label()
            object3D.faces[h_coord[0]].sides[h_coord[1]].label_winding = \
                h_coord[2]
            object3D.faces[h_coord[0]].sides[h_coord[1]].label = \
                object3D.labels[2]
            object3D.faces[h_coord[0]].sides[h_coord[1]].lock_label()

        matrix = {
            'top-right': [[1, 0, k * sin(radians(α))],
                          [0, 1, k * cos(radians(α))]],
            'bottom-right': [[1, 0, k * sin(radians(α))],
                             [0, 1, -k * cos(radians(α))]],
            'bottom-left': [[1, 0, -k * sin(radians(α))],
                            [0, 1, -k * cos(radians(α))]],
            'top-left': [[1, 0, -k * sin(radians(α))],
                         [0, 1, k * cos(radians(α))]]
        }[direction]
        self._edges = []
        self._edges3D = {}
        self._vertices = []
        self._vertices_match = {}

        def project(point, matrix):
            x = sum([
                pc * coord for pc, coord in zip(matrix[0], point.coordinates)
            ])
            y = sum([
                pc * coord for pc, coord in zip(matrix[1], point.coordinates)
            ])
            return Point(x, y, point.name)

        for vertex in object3D.vertices:
            projected_point = project(vertex, matrix)
            self._vertices.append(projected_point)
            self._vertices_match[vertex.name] = (vertex, projected_point)

        # To store to which edges a vertex belongs
        vertices_connexions = {k: [] for k in self._vertices}

        # Build the projected edges
        for edge in object3D.edges:
            p0 = self._vertices_match[edge.endpoints[0].name][1]
            p1 = self._vertices_match[edge.endpoints[1].name][1]
            # TODO: check cases when the projected edge is a single point
            # (ZeroBipoint should be raised)
            projected_edge = LineSegment(p0,
                                         p1,
                                         thickness=thickness,
                                         draw_endpoints=draw_vertices,
                                         label_endpoints=label_vertices,
                                         color=color,
                                         allow_zero_length=False,
                                         locked_label=True,
                                         label_scale=edge.label_scale,
                                         label=edge.label,
                                         label_mask=edge.label_mask,
                                         label_winding=edge.label_winding,
                                         sloped_label=False)
            vertices_connexions[p0].append(LineSegment(p0, p1))
            vertices_connexions[p1].append(LineSegment(p1, p0))
            if projected_edge not in self._edges:  # TODO: else, what...?
                self._edges.append(projected_edge)
            if projected_edge not in self._edges3D:  # TODO: else, what...?
                self._edges3D[projected_edge] = edge
        # Find out which edges are hidden.
        # The ones that belong to convex hull of the projected vertices are
        # considered visible. By default, they will remain visible (i.e. keep
        # the default 'solid' dashpattern).
        points_cloud = set()  # to avoid duplicates
        for edge in self.edges:
            points_cloud.update(edge.endpoints)
        cvh = convex_hull(*points_cloud)
        for edge in self.edges:
            # Only edges not belonging to the convex hull may be hidden
            if not (edge.endpoints[0] in cvh and edge.endpoints[1] in cvh):
                m = self._edges3D[edge].midpoint()
                pm = project(m, matrix)  # projected midpoint
                # Check if the midpoint of the tested edge is behind (i.e.
                # deeper) than a face while being inside it
                for f in object3D.faces:
                    pface = [project(v, matrix) for v in f.vertices]
                    if (all([v.z <= m.z for v in f.vertices])
                            and pm not in convex_hull(pm, *pface)
                            and not any(m.belongs_to(s) for s in f.sides)):
                        edge.dashpattern = \
                            config.oblique_projection.DASHPATTERN

        # Setup the vertices' labels
        for vertex in self.vertices:
            edges = sorted(vertices_connexions[vertex],
                           key=lambda edge: Vector(edge).slope360)
            couples = [(edges[n], edges[(n + 1) % len(edges)])
                       for n, _ in enumerate(edges)]
            widest = max(couples,
                         key=lambda couple: Vector(couple[0]).angle_measure(
                             Vector(couple[1])))
            u = Vector(vertex, widest[0].endpoints[1])
            v = Vector(vertex, widest[1].endpoints[1])
            vertex.label_position = \
                tikz_approx_position(u.bisector(v).slope360)
def test_equality():
    """Check __eq__() is correct."""
    assert Vector(1, 1) != Bipoint(Point(0, 0), Point(1, 1))
    assert Vector(1, 1) == Vector(Point(1, 1), Point(2, 2))
def test_neg():
    """Check Vector.__neg__()"""
    u = Vector(2, 7)
    assert -u == Vector(-2, -7)
    u = Vector(-1, 8, -5)
    assert -u == Vector(1, -8, 5)
def test_repr():
    """Check Vector.__repr__()"""
    u = Vector(Bipoint(Point(0, 0), Point(2, 5)))
    assert repr(u) == 'Vector(2, 5)'
    u = Vector(Bipoint(Point(0, 0, 0), Point(2, 5)))
    assert repr(u) == 'Vector(2, 5, 0)'
def test_bisector():
    """Check bisector of two Vectors."""
    Ω = Point(0, 0)
    pointI = Point(1, 0)
    J = Point(0, 1)
    A = Point(1, 1)
    i = Vector(Ω, pointI)
    j = Vector(Ω, J)
    a = Vector(Ω, A)
    assert i.bisector(j) == a
    assert j.bisector(i) == -a
    k = Vector(Ω, Point(2, 0))
    assert k.bisector(j) == a
    with pytest.raises(TypeError) as excinfo:
        k.bisector('j')
    assert str(excinfo.value) == 'Can only create the bisector with another ' \
        'Vector. Found \'j\' instead.'
示例#15
0
    def __init__(self, point, vertex, point_or_measure, decoration=None,
                 mark_right=False, second_point_name='auto', label=None,
                 color=None, thickness='thick', armspoints=None,
                 label_vertex=False, draw_vertex=False,
                 label_armspoints=False, draw_armspoints=False,
                 label_endpoints=False, draw_endpoints=False,
                 naming_mode='from_endpoints', decoration2=None):
        """
        :param point: a Point of an arm of the Angle
        :type point: Point
        :param vertex: the Angle's vertex
        :type vertex: Point
        :param point_or_measure: either a Point of the other arm of the Angle,
        or the measure of the Angle
        :type point_or_measure: Point or number
        :param decoration: the decoration of the Angle
        :type decoration: None or AngleDecoration
        :param mark_right: to tell whether to mark the angle as a right angle
        :type mark_right: bool
        :param second_point_name: Only used if point_or_measure is a measure,
        this is the name of the 2d arm's Point. If set to 'auto', then the name
        of the first Point will be used, concatenated to a '.
        :type second_point_name: str
        :param thickness: the Angle's arms' thickness. Available values are
        TikZ's ones.
        :type thickness: str
        :param color: the color of the Angle's arms.
        :type color: str
        :param naming_mode: how to build the name. Possible modes are:
        'from_endpoints', 'from_armspoints', 'from_vertex'. Note that if no
        armspoints are defined, then trying to get the Angle.name will raise an
        error
        :type naming_mode: str
        """
        self.color = color
        self.thickness = thickness
        self.naming_mode = naming_mode
        self.decoration = decoration
        self.decoration2 = decoration2
        # The label must be set *after* the possible decoration, because it
        # will actually be handled by self.decoration
        if (self.decoration is None
            or self.decoration.label in [None, 'default']):
            self.label = label
        else:
            if label is not None:
                raise ValueError('The label has been set twice, as Angle\'s '
                                 'keyword argument ({}) and as its '
                                 'AngleDecoration\'s keyword argument ({}).'
                                 .format(repr(label),
                                         repr(self.decoration.label_value)))
        self.mark_right = mark_right
        self.label_vertex = label_vertex
        self.label_endpoints = label_endpoints
        self.draw_endpoints = draw_endpoints
        self.label_armspoints = label_armspoints
        self.draw_armspoints = draw_armspoints
        self.draw_vertex = draw_vertex
        if not (isinstance(point, Point)
                and isinstance(vertex, Point)
                and (isinstance(point_or_measure, Point)
                     or is_number(point_or_measure))):
            raise TypeError('Three Points, or two Points and the measure of '
                            'the angle are required to build an Angle. '
                            'Found instead: {}, {} and {}.'
                            .format(type(point), type(vertex),
                                    type(point_or_measure)))
        self._points = [point, vertex]
        if isinstance(point_or_measure, Point):
            self._points.append(point_or_measure)
        else:
            self._points.append(point.rotate(vertex, point_or_measure,
                                             rename=second_point_name))

        if any([p.three_dimensional for p in self._points]):
            self._three_dimensional = True
        else:
            self._three_dimensional = False

        # Measure of the angle:
        if self._three_dimensional:
            u = Vector(self.points[1], self.points[0])
            v = Vector(self.points[1], self.points[2])
            self._measure = Number(str(degrees(atan2(u.cross(v).length,
                                                     u.dot(v)))))
        else:  # 2D angles measure
            p0 = Point(self._points[0].x - self._points[1].x,
                       self._points[0].y - self._points[1].y,
                       None)
            p2 = Point(self._points[2].x - self._points[1].x,
                       self._points[2].y - self._points[1].y,
                       None)
            α0 = Number(str(degrees(atan2(p0.y, p0.x))))
            α2 = Number(str(degrees(atan2(p2.y, p2.x))))
            self._measure = α2 - α0

        if self._measure < 0:
            self._measure += 360

        # This is not like the matching Triangle!
        if shoelace_formula(*self.points) > 0:
            self.winding = 'clockwise'
        else:
            self.winding = 'anticlockwise'

        arm0 = Bipoint(self._points[1], self._points[0])
        arm1 = Bipoint(self._points[1], self._points[2])
        self._arms = [arm0, arm1]
        self.armspoints = armspoints

        # Only 2D: labels positioning
        if not self.three_dimensional:
            # Vertex' label positioning
            bisector = Vector(self._points[0], self.vertex)\
                .bisector(Vector(self._points[2], self.vertex),
                          new_endpoint_name=None)
            try:
                self._points[1].label_position = \
                    tikz_approx_position(bisector.slope360)
            except ZeroVector:
                self._points[1].label_position = \
                    tikz_approx_position(
                        Bipoint(self.vertex,
                                self._points[0].rotate(self.vertex, -90,
                                                       rename=None)
                                ).slope360)

            # Endpoints labels positioning
            direction = 1 if self.winding == 'anticlockwise' else -1
            self.endpoints[0].label_position = \
                tikz_approx_position(arm0.slope360 - direction * 55)
            self.endpoints[1].label_position = \
                tikz_approx_position(arm1.slope360 + direction * 55)