def __init__(self, *args, allow_zero_length=True): """ It's possible to create a Vector giving: - a Bipoint: Bipoint(A, B) - a pair of Points: A, B - its coordinates x, y or x, y, z """ if not args or len(args) >= 4: raise TypeError('Vector() takes one, two or three arguments ' '({} given)'.format(len(args))) if len(args) == 1: from mathmakerlib.geometry.bipoint import Bipoint if not isinstance(args[0], Bipoint): raise TypeError('a Vector can be created from one Bipoint, ' 'found {} instead.'.format(repr(args[0]))) self._x = args[0].Δx self._y = args[0].Δy if args[0].three_dimensional: self._z = args[0].Δz self._three_dimensional = True else: self._z = Number(0) self._three_dimensional = False elif len(args) == 2: # Two Points if isinstance(args[0], Point) and isinstance(args[1], Point): self._x = args[1].x - args[0].x self._y = args[1].y - args[0].y if args[0].three_dimensional or args[1].three_dimensional: self._three_dimensional = True self._z = args[1].z - args[0].z else: self._three_dimensional = False self._z = Number(0) # Two numbers elif is_number(args[0]) and is_number(args[1]): self._three_dimensional = False self._x = Number(args[0]) self._y = Number(args[1]) self._z = Number(0) else: raise TypeError('a Vector can be created from two arguments, ' 'either two Points or two numbers. ' 'Found {} and {} instead.'.format( repr(args[0]), repr(args[1]))) elif len(args) == 3: self._three_dimensional = True self._x = Number(args[0]) self._y = Number(args[1]) self._z = Number(args[2]) self._length = Number(self.x**2 + self.y**2 + self.z**2).sqrt() if not allow_zero_length and self.length == 0: msg = 'Explicitly disallowed creation of a zero-length {}.'\ .format(type(self).__name__) raise ZERO_OBJECTS_ERRORS[type(self).__name__](msg)
def move_fracdigits_to(n, from_nb=None): """ Turn n into decimal instead of all decimals found in the from_nb list. Each decimal found in the numbers' list will be recursively replaced by 10 times itself (until it is no decimal anymore) while in the same time n will be divided by 10. This is useful for the case division by a decimal is unwanted. :param n: the number who will be divided by 10 instead of the others :type n: any number (int, Decimal, float though they're not advised) :param from_nb: an iterable containing the numbers that must be integers :type from_nb: a list (of numbers) :rtype: a list (of numbers) """ if type(from_nb) is not list: raise TypeError('A list of numbers must be given as argument ' '\'numbers\'.') if not is_number(n): raise TypeError('The first argument must be a number.') n = Decimal(str(n)) # If any element of from_nb is not a number, is_integer() will raise # an exception. if all([is_integer(i) for i in from_nb]): return [ n, ] + [i for i in from_nb] numbers_copy = copy.deepcopy(from_nb) for i, j in enumerate(from_nb): if not is_integer(j): numbers_copy[i] = j * 10 return move_fracdigits_to(n / 10, from_nb=numbers_copy)
def dividing_points(self, n=None, prefix='a'): """ Create the list of Points that divide the Bipoint in n parts. :param n: the number of parts (so it will create n - 1 points) n must be greater or equal to 1 :type n: int """ if not (is_number(n) and is_integer(n)): raise TypeError('n must be an integer') if not n >= 1: raise ValueError('n must be greater or equal to 1') x0 = self.points[0].x x1 = self.points[1].x xstep = (x1 - x0) / n x_list = [x0 + (i + 1) * xstep for i in range(int(n - 1))] y0 = self.points[0].y y1 = self.points[1].y ystep = (y1 - y0) / n y_list = [y0 + (i + 1) * ystep for i in range(int(n - 1))] if self.three_dimensional: z0 = self.points[0].z z1 = self.points[1].z zstep = (z1 - z0) / n z_list = [z0 + (i + 1) * zstep for i in range(int(n - 1))] else: z_list = ['undefined' for i in range(int(n - 1))] return [ Point(x, y, z, prefix + str(i + 1)) for i, (x, y, z) in enumerate(zip(x_list, y_list, z_list)) ]
def radius(self, value): from mathmakerlib.calculus.number import Number if is_number(value): self._radius = Number(value) else: raise TypeError( 'Expected a number as radius. Got {} instead.'.format( str(type(value))))
def __new__(cls, sign=None, numerator=None, denominator=None, from_decimal=None): """ Fraction(5, 8) is equivalent to Fraction('+', 5, 8). """ if from_decimal is not None: f = Number(10)**max(1, from_decimal.fracdigits_nb()) sign = Sign(from_decimal) numerator = (f * from_decimal).standardized() denominator = f.standardized() elif sign in ['+', '-']: if not (is_number(numerator) and is_number(denominator)): raise TypeError('Numerator and denominator must be numbers. ' 'Got {} and {} instead.'.format( type(numerator), type(denominator))) if not (is_integer(numerator) and is_integer(denominator)): raise TypeError('Numerator and denominator must be integers. ' 'Got {} and {} instead.'.format( numerator, denominator)) else: if not (is_number(sign) and is_number(numerator)): raise TypeError('Numerator and denominator must be numbers. ' 'Got {} and {} instead.'.format( type(sign), type(numerator))) elif not (is_integer(sign) and is_integer(numerator)): raise TypeError('Numerator and denominator must be integers. ' 'Got {} and {} instead.'.format( sign, numerator)) denominator = numerator numerator = sign sign = '+' # some initialization self = object.__new__(cls) self._sign = Sign(sign) self._numerator = Number(numerator) self._denominator = Number(denominator) return self
def reduced_by(self, n): """Return Fraction reduced by n (possibly return a Number).""" if not is_number(n): raise TypeError('A Fraction can be reduced only by an integer, ' 'got {} instead.'.format(type(n))) if not is_integer(n): raise TypeError('A Fraction can be reduced only by an integer, ' 'got {} instead.'.format(n)) if not is_integer(self.numerator / n): raise ValueError( 'Cannot divide {} by {} and get an integer.'.format( self.numerator, n)) if not is_integer(self.denominator / n): raise ValueError( 'Cannot divide {} by {} and get an integer.'.format( self.denominator, n)) n = abs(n) if self.denominator / n == 1: return self.sign * self.numerator / n if self.denominator / n == -1: return Sign('-') * self.sign * self.numerator / n return Fraction(self.sign, self.numerator / n, self.denominator / n)
def point_at(self, position, name='automatic'): """ A Point aligned with the Bipoint, at provided position. The Bipoint's length is the length unit of position. Hence, position 0 matches points[0], position 1 matches points[1], position 0.5 matches the midpoint, position 0.75 is three quarters on the way from points[0] to points[1], position 2 is a Point that makes points[1] the middle between it and points[0], position -1 makes points[0] the middle between it and points[1]. :param position: a number :type position: number :param name: the name to give to the Point :type name: str """ if not is_number(position): raise TypeError( 'position must be a number, found {} instead.'.format( type(position))) k = Number(position) if k == 0: return self.points[0] elif k == 1: return self.points[1] else: if self.three_dimensional: zval = (self.points[0].z + (self.points[1].z - self.points[0].z) * k) else: zval = 'undefined' return Point( (self.points[0].x + (self.points[1].x - self.points[0].x) * k), (self.points[0].y + (self.points[1].y - self.points[0].y) * k), z=zval, name=name)
def rotate(self, center, angle, axis=None, rename='auto'): """ Rotate around center (or axis going through center). :param center: the center of the rotation :type center: Point :param angle: the angle of the rotation :type angle: a number :param axis: the axis of the rotation for 3D rotation. If left to None, the rotation happens around the center, in the plane. :type axis: Vector :param rename: if set to 'auto', will name the rotated Point after the original, adding a ' (like A.rotate(...) creates a Point A'). If set to None, keep the original name. Otherwise, the provided str will be used as the rotated Point's name. :type rename: None or str :rtype: Point """ from mathmakerlib.geometry.vector import Vector if not isinstance(center, Point): raise TypeError('Expected a Point as rotation center, got {} ' 'instead.'.format(type(center))) if not is_number(angle): raise TypeError('Expected a number as rotation angle, got {} ' 'instead.'.format(type(angle))) if not (axis is None or isinstance(axis, Vector)): raise TypeError('Expected either None or a Vector as axis, ' 'found {} instead.'.format(repr(axis))) Δx = self.x - center.x Δy = self.y - center.y Δz = self.z - center.z cosθ = Number(str(cos(radians(angle)))) sinθ = Number(str(sin(radians(angle)))) if axis is None: rx = (Δx * cosθ - Δy * sinθ + center.x).rounded(Number('1.000')) ry = (Δx * sinθ + Δy * cosθ + center.y).rounded(Number('1.000')) rz = 'undefined' else: ux, uy, uz = axis.normalized().coordinates rotation_matrix = [[ cosθ + (1 - cosθ) * ux**2, ux * uy * (1 - cosθ) - uz * sinθ, ux * uz * (1 - cosθ) + uy * sinθ ], [ uy * ux * (1 - cosθ) + uz * sinθ, cosθ + (1 - cosθ) * uy**2, uy * uz * (1 - cosθ) - ux * sinθ ], [ uz * ux * (1 - cosθ) - uy * sinθ, uz * uy * (1 - cosθ) + ux * sinθ, cosθ + (1 - cosθ) * uz**2 ]] rx = sum([ rc * coord for rc, coord in zip(rotation_matrix[0], [Δx, Δy, Δz]) ]) ry = sum([ rc * coord for rc, coord in zip(rotation_matrix[1], [Δx, Δy, Δz]) ]) rz = sum([ rc * coord for rc, coord in zip(rotation_matrix[2], [Δx, Δy, Δz]) ]) rx += center.x ry += center.y rz += center.z rx = rx.rounded(Number('1.000')) ry = ry.rounded(Number('1.000')) rz = rz.rounded(Number('1.000')) if rename == 'keep_name': rname = self.name elif rename == 'auto': rname = self.name + "'" else: rname = rename return Point(rx, ry, rz, rname)
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 __init__(self, start_vertex=None, name=None, side_length=Number('1'), build_angle=60, mark_equal_sides=True, use_mark='||', draw_vertices=False, label_vertices=True, thickness='thick', color=None, rotation_angle=0, winding=None, sloped_sides_labels=True): r""" Initialize Rhombus :param start_vertex: the vertex to start to draw the Rhombus (default (0; 0)) :type start_vertex: Point :param name: the name of the Rhombus, like ABCD. Can be either None (the names will be automatically created), or a string of the letters to use to name the vertices. Only single letters are supported as Points' names so far (at Polygon's creation). See issue #3. :type name: None or str :param side_length: the length that will be used to calculate the coordinates of the vertices used to build the Rhombus :type side_length: a number :param build_angle: one of the interior angles of the Rhombus. :type build_angle: any number :param mark_equal_sides: if True (default), all four sides will be automatically marked with the same symbol. :type mark_equal_sides: bool :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 Quadrilateral's sides :type thickness: str :param color: the color of the Quadrilateral's sides :type color: str :param rotate: the angle of rotation around isobarycenter :type rotate: int """ if start_vertex is None: start_vertex = Point(0, 0) self._side_length = Number(side_length) if is_number(build_angle): self._build_angle = build_angle else: raise TypeError( 'Expected an integer as build_angle, found {}.'.format( type(build_angle))) x = (side_length * Number(str(cos(radians(build_angle / 2)))))\ .rounded(Number('0.001')) y = (side_length * Number(str(sin(radians(build_angle / 2)))))\ .rounded(Number('0.001')) v1 = Point(x + start_vertex.x, -y + start_vertex.y) v2 = Point(2 * x + start_vertex.x, start_vertex.y) v3 = Point(x + start_vertex.x, y + start_vertex.y) Quadrilateral.__init__(self, start_vertex, v1, v2, v3, name=name, draw_vertices=draw_vertices, label_vertices=label_vertices, thickness=thickness, color=color, rotation_angle=rotation_angle, winding=winding, sloped_sides_labels=sloped_sides_labels) self._type = 'Rhombus' Equilateral.__init__(self, mark_equal_sides=mark_equal_sides, use_mark=use_mark)
def check_scale(value, source_name): if not is_number(value): raise TypeError( 'The {}\'s scale must be a number.'.format(source_name))
def n(self, n): if not (is_number(n) and is_integer(n) and n >= 1): raise TypeError('n must be an integer >= 1, got {} instead.' .format(n)) self._n = Number(n)