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)
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
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.'
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)