def tikz_declarations(self): """Return the Point declaration.""" if self.name is None: raise RuntimeError('Point at ({}, {}) has no name (None), ' 'cannot create TikZ picture using it.') return r'\coordinate ({}) at ({},{});'\ .format(self.name, self.x.rounded(Number('0.001')), self.y.rounded(Number('0.001')))
def _slope(self, offset=0): if self.length == 0: msg = 'Cannot calculate the slope of a zero-length {}.'\ .format(type(self).__name__) raise ZERO_OBJECTS_ERRORS[type(self).__name__](msg) theta = Number( str(math.degrees(math.acos(self.x / self.length))))\ .rounded(Number('0.001')) return theta if self.y >= 0 else Number(offset) - theta
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 boundingbox(self, value): if value is not None: if not isinstance(value, tuple): raise TypeError('Expected a tuple, found a {} instead.'.format( type(value).__name__)) if len(value) != 4: raise ValueError('Expected a tuple of 4 elements, found {} ' 'elements instead.'.format(len(value))) for v in value: try: Number(v) except (TypeError, InvalidOperation): raise TypeError('Expected a tuple containing only ' 'numbers. Found a {} instead.'.format( type(v).__name__)) setattr(self, '_boundingbox', tuple(Number(v) for v in value))
def tikz_rightangle_mark(self, winding='anticlockwise'): if self.decoration is None or not self.mark_right: return '' check_winding(winding) # Decimal numbers in TikZ must be written with a dot as decimal point. # As of now, there is no reliable way to temporarily change the # locale to 'C' (or 'en_US'), so here's a little patch that will # replace possibly other decimal points by a '.'. theta = str(Bipoint(self.vertex, self.points[0]) .slope.rounded(Number('0.01')).printed) if (locale.localeconv()['decimal_point'] != '.' and locale.localeconv()['decimal_point'] in theta): theta = theta.replace(locale.localeconv()['decimal_point'], '.') rt = 'cm={{cos({θ}), sin({θ}), -sin({θ}), cos({θ}), ({v})}}' \ .format(θ=theta, v=self.vertex.name) draw_options = tikz_options_list([self.decoration.thickness, self.decoration.color, rt]) if winding == 'anticlockwise': rightangle_shape = '({R}, 0) -- ({R}, {R}) -- (0, {R})'\ .format(R=self.decoration.radius.uiprinted) elif winding == 'clockwise': rightangle_shape = '({R}, 0) -- ({R}, -{R}) -- (0, -{R})'\ .format(R=self.decoration.radius.uiprinted) return '\draw{} {};'.format(draw_options, rightangle_shape)
def generate_tikz(self, *points_names): if not len(points_names) == 3: raise RuntimeError('Three Points\' names must be provided to ' 'generate the AngleDecoration. Found {} ' 'arguments instead.'.format(len(points_names))) last_layer = {None: 1, 'single': 1, 'double': 2, 'triple': 3}[self.variety] pic_attr = self.tikz_attributes(do_label=last_layer == 1) if pic_attr == '[]': return '' required.tikz_library['angles'] = True deco = ['pic {} {{angle = {}--{}--{}}}' .format(pic_attr, *points_names)] if self.variety in ['double', 'triple']: space_sep = Number('0.16') deco.append('pic {} {{angle = {}--{}--{}}}' .format(self.tikz_attributes( radius_coeff=1 + space_sep, do_label=last_layer == 2), *points_names)) if self.variety == 'triple': deco.append('pic {} {{angle = {}--{}--{}}}' .format(self.tikz_attributes( radius_coeff=1 + 2 * space_sep, do_label=last_layer == 3), *points_names)) return deco
def tikz_attributes(self, radius_coeff=1, do_label=True): if not is_number(radius_coeff): raise TypeError('radius_coeff must be a number, found {} instead.' .format(type(radius_coeff))) attributes = [] if do_label and self.label not in [None, 'default']: required.tikz_library['quotes'] = True attributes.append('"{}"'.format(self.label)) if self.eccentricity is not None: attributes.append('angle eccentricity={}' .format(self.eccentricity)) if self.variety is not None: if self.do_draw: attributes.append('draw') if self.arrow_tips is not None: attributes.append(self.arrow_tips) if self.thickness is not None: attributes.append(self.thickness) if self.radius is not None: attributes.append('angle radius = {}' .format((self.radius * radius_coeff) .rounded(Number('0.01')).uiprinted)) if self.hatchmark is not None: attributes.append(self.hatchmark) required.tikz_library['decorations.markings'] = True required.tikzset[self.hatchmark + '_hatchmark'] = True if (self.variety is not None or (do_label and self.label not in [None, 'default'])): if self.color is not None: attributes.append(self.color) return '[{}]'.format(', '.join(attributes))
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 lbl_perimeter(self): if any([not isinstance(s.label_value, Number) for s in self.sides]): raise RuntimeError('All labels must have been set as Numbers ' 'in order to calculate the perimeter from ' 'labels.') else: return sum([s.label_value for s in self.sides], Number(0, unit=self.sides[0].label_value.unit))\ .standardized()
def gap(self, value): if not (is_number(value) or value is None): raise TypeError('The gap value must be None or a number. ' 'Found {} instead (type: {}).' .format(repr(value), type(value))) if value is None: self._gap = None else: self._gap = Number(value)
def radius(self, value): if is_number(value): self._radius = Number(value) elif value is None: self._radius = None else: raise TypeError('Expected a number as radius. Got {} instead.' .format(str(type(value)))) if hasattr(self, '_eccentricity') and hasattr(self, '_gap'): self.eccentricity = 'automatic'
def z(self, value): three_dimensional = True if value is 'undefined': value = 0 three_dimensional = False try: self._z = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as applicate, found {} ' 'instead.'.format(repr(value))) self._three_dimensional = three_dimensional
def eccentricity(self, value): if value == 'automatic': if self.gap is None: raise ValueError('Cannot calculate the eccentricity if gap ' 'is None.') value = (self.gap / self.radius + 1)\ .rounded(Number('0.01')).standardized() if not (value is None or is_number(value)): raise TypeError('The eccentricity of an AngleDecoration must be ' 'None or a Number. Found {} instead.' .format(type(value))) self._eccentricity = value
def __init__(self, color=None, thickness='thick', label='default', radius=Number('0.25', unit='cm'), variety='single', gap=Number('0.4', unit='cm'), eccentricity='automatic', hatchmark=None, do_draw=True, arrow_tips=None): self.do_draw = do_draw self.arrow_tips = arrow_tips self.color = color self.thickness = thickness self.label = label self.radius = radius self.gap = gap if gap is None: self.gap = None else: u = self.radius.unit if isinstance(self.radius, Number) else None self.gap = Number(gap, unit=u) # Eccentricity must be set *after* radius, in order to be able to # calculate a reasonable default eccentricity based on the radius self.eccentricity = eccentricity self.variety = variety self.hatchmark = hatchmark
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 tikz_approx_position(slope): slope %= Number(360) # Caution: modulo on negative Decimals does not behave as on ints. # So, it's necessary to add 360 in case of a negative result. if slope < 0: slope += 360 if (Decimal('337.5') <= slope <= Decimal('360') or Decimal('0') <= slope < Decimal('22.5')): return 'right' elif Decimal('22.5') <= slope < Decimal('67.5'): return 'above right' elif Decimal('67.5') <= slope < Decimal('112.5'): return 'above' elif Decimal('112.5') <= slope < Decimal('157.5'): return 'above left' elif Decimal('157.5') <= slope < Decimal('202.5'): return 'left' elif Decimal('202.5') <= slope < Decimal('247.5'): return 'below left' elif Decimal('247.5') <= slope < Decimal('292.5'): return 'below' elif Decimal('292.5') <= slope < Decimal('337.5'): return 'below right'
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 mark_scale(self, value): check_scale(value, 'LineSegment\'s mark') self._mark_scale = Number(value)
def __init__(self): self.DEFAULT_ARMSPOINTS_POSITION = Number('0.8')
def shape_scale(self, value): check_scale(value, 'Point\'s shape') self._shape_scale = Number(value)
def x(self, value): try: self._x = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as abscissa, found {} ' 'instead.'.format(repr(value)))
def y(self, value): try: self._y = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as ordinate, found {} ' 'instead.'.format(repr(value)))
def __init__(self, start_vertex=None, name=None, base_length=Number('1.5'), equal_legs_length=Number('1'), 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 Isosceles Triangle :param start_vertex: the vertex to start to draw the Right Triangle (default (0; 0)) :type start_vertex: Point :param name: the name of the Triangle, like ABC. 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 base_length: the length of the base of the IsoscelesTriangle, that will be used to calculate the coordinates of its vertices :type base_length: a number :param equal_legs_length: the length of the equal legs of the IsoscelesTriangle, that will be used to calculate the coordinates of its vertices :type equal_legs_length: a number :param mark_equal_sides: if True (default), all three 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 Triangle's sides :type thickness: str :param color: the color of the Triangle'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._base_length = Number(base_length) self._equal_legs_length = Number(equal_legs_length) v1 = Point(base_length + start_vertex.x, start_vertex.y) v2 = Point( base_length / 2, (equal_legs_length**2 - Number('0.25') * base_length**2).sqrt().rounded(Number('0.001')) + start_vertex.y) if (winding == 'clockwise' or (winding is None and config.polygons.DEFAULT_WINDING == 'clockwise')): start_vertex, v1 = v1, start_vertex Triangle.__init__(self, start_vertex, v1, v2, 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 = 'IsoscelesTriangle' if mark_equal_sides: self.sides[1].mark = self.sides[2].mark = use_mark
class Point(Drawable, Dimensional): names_in_use = set() @classmethod def automatic_names(cls): use = Point.names_in_use names = [ letter for letter in string.ascii_uppercase if letter not in use ][::-1] if not names: layer = 1 while not names: names = [ '{}$_{}$'.format(letter, layer) for letter in string.ascii_uppercase if '{}$_{}$'.format(letter, layer) not in use ][::-1] layer += 1 return names @classmethod def reset_names(cls): cls.names_in_use = set() def __init__(self, x=None, y=None, z='undefined', name='automatic', color=None, shape=r'$\times$', shape_scale=Number('0.67'), label='default', label_position='below'): r""" Initialize Point When not naming the keyword arguments, it is possible to not mention the applicate, z: Point(3, 2, 'A') is almost equivalent to Point(3, 2, 0, 'A'): - all subsequent keyword arguments must be named - the first version is a 2D Point, the second version a 3D Point. :param x: the Point's abscissa :type x: anything that can be turned to a Number :param y: the Point's ordinate :type y: anything that can be turned to a Number :param z: the Point's applicate :type z: anything that can be turned to a Number :param name: the Point's name (e.g. 'A'). If it's left to 'automatic', a yet unused name will be set. :type name: str :param shape: the symbol that will be drawn at the Point's position. Default value is '$\times$', what draws a cross. :type shape: str :param label: what will be placed near the Point. The default value will set the Point's name as label. Setting label at '' will disable labeling the Point. :type label: str :param label_position: if any label is to be drawn, this is where to draw it. Available values are TikZ's ones ('above', 'below left'...). :type label_position: str """ self._three_dimensional = False self._name = None self._x = None self._y = None self._z = None self._shape = None self._label = None self._label_position = None self.x = x self.y = y try: self.z = z except (TypeError, InvalidOperation): # The third value cannot be used as z; let's assume it is actually # 2D geometry and the value is actually simply the name, implying # z should be set to 0. self.z = Number(0) self._three_dimensional = False self.name = z else: self.name = name self.shape = shape if label is 'default': self.label = self.name else: self.label = label self.label_position = label_position if color is not None: self.color = color self.shape_scale = shape_scale def __str__(self): if not self.three_dimensional: s = '{}({}, {})'.format(self.name, self.x, self.y) else: s = '{}({}, {}, {})'.format(self.name, self.x, self.y, self.z) return s def __repr__(self): if not self.three_dimensional: s = 'Point {}({}, {})'.format(self.name, self.x, self.y) else: s = 'Point {}({}, {}, {})'.format(self.name, self.x, self.y, self.z) return s def __hash__(self): if not self.three_dimensional: s = 'Point ({}, {})'.format(self.x, self.y) else: s = 'Point ({}, {}, {})'.format(self.x, self.y, self.z) return hash(s) def __eq__(self, other): if isinstance(other, Point): p = config.points.DEFAULT_POSITION_PRECISION return all([ self.x.rounded(p) == other.x.rounded(p), self.y.rounded(p) == other.y.rounded(p), self.z.rounded(p) == other.z.rounded(p) ]) else: return False @property def name(self): return self._name @name.setter def name(self, value): if self._name is not None: Point.names_in_use.discard(self._name) if value is None: self._name = None else: value = str(value) if value == 'automatic': self._name = Point.automatic_names().pop() else: self._name = value Point.names_in_use.add(self._name) @property def x(self): return self._x @x.setter def x(self, value): try: self._x = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as abscissa, found {} ' 'instead.'.format(repr(value))) @property def y(self): return self._y @y.setter def y(self, value): try: self._y = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as ordinate, found {} ' 'instead.'.format(repr(value))) @property def z(self): return self._z @z.setter def z(self, value): three_dimensional = True if value is 'undefined': value = 0 three_dimensional = False try: self._z = Number(value) except (TypeError, InvalidOperation): raise TypeError('Expected a number as applicate, found {} ' 'instead.'.format(repr(value))) self._three_dimensional = three_dimensional @property def coordinates(self): return (self._x, self._y, self._z) @property def shape(self): return self._shape @shape.setter def shape(self, other): self._shape = str(other) @property def shape_scale(self): return self._shape_scale @shape_scale.setter def shape_scale(self, value): check_scale(value, 'Point\'s shape') self._shape_scale = Number(value) @property def label_position(self): return self._label_position @label_position.setter def label_position(self, other): if other is None: self._label_position = None else: self._label_position = str(other) 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 belongs_to(self, other): """Check if the Point belongs to a LineSegment.""" # This could be extended to Lines and Rays, later. # Then check if self is different from the 2 known Points, and # if it belongs to the Line, then: # (x - x1) / (x2 - x1) = (y - y1) / (y2 - y1) # also, if three_dimensional, all this = (z - z1) / (z2 - z1) from mathmakerlib.geometry import LineSegment if not isinstance(other, LineSegment): raise TypeError('Argument \'other\' must be a LineSegment. ' 'Found {} instead.'.format(repr(other))) d1 = LineSegment(self, other.endpoints[0]).length d2 = LineSegment(self, other.endpoints[1]).length return d1 + d2 == other.length def tikz_declaring_comment(self): """ Replace plural by singular in the declaring comment. :rtype: str """ return '% Declare Point' def tikz_declarations(self): """Return the Point declaration.""" if self.name is None: raise RuntimeError('Point at ({}, {}) has no name (None), ' 'cannot create TikZ picture using it.') return r'\coordinate ({}) at ({},{});'\ .format(self.name, self.x.rounded(Number('0.001')), self.y.rounded(Number('0.001'))) def tikz_drawing_comment(self): """Return the comment preceding the Point's drawing.""" return ['% Draw Point'] def _tikz_draw_options(self): return [self.color] def tikz_draw(self): """Return the command to actually draw the Point.""" sh_scale = '' if self.shape_scale != 1: sh_scale = '[scale={}]'.format(self.shape_scale) return [ r'\draw{} ({}) node{} {};'.format(tikz_options_list('draw', self), self.name, sh_scale, '{' + self.shape + '}') ] def tikz_labeling_comment(self): """ Replace plural by singular in the labeling comment. :rtype: str """ return '% Label Point' def tikz_points_labels(self): return self.tikz_label() def tikz_label(self): """Return the command to write the Point's label.""" if self.label is None: return '' else: if self.label_position is None: label_position = '' else: label_position = '[' + self.label_position + ']' return r'\draw ({}) node{} {};'\ .format(self.label, label_position, '{' + self.label + '}')
def __init__(self, *points, thickness='thick', dashpattern='solid', label=None, label_mask=None, label_winding='anticlockwise', label_position=None, label_scale=None, mark=None, mark_scale=Number('0.5'), color=None, draw_endpoints=True, label_endpoints=True, locked_label=False, allow_zero_length=True, sloped_label=True): """ Initialize LineSegment :param points: either one LineSegment to copy, or two Points :type points: another LineSegment or a list or tuple of two Points :param thickness: the LineSegment's thickness. Available values are TikZ's ones. :type thickness: str :param label: what will be written along the LineSegment, about its middle. A None value will disable the LineSegment's labeling. :type label: None or str :param label_mask: if not None (default), hide the label with ' ' or '?' :type label_mask: None or str (' ' or '?') :param label_position: tells where to put the LineSegment's label. Can be a value used by TikZ or 'clockwise' or 'anticlockwise'. 'anticlockwise' (default) will automatically set the label_position to 'above' if Δx < 0 and to 'below' if Δx > 0. This is useful to put all LineSegments' labels outside a Polygon that is drawn in an anticlockwise manner. Same for 'clockwise', in the reversed direction. :type label_position: str :param label_scale: the label's scale :type label_scale: None or anything that is accepted to create a Number :param draw_endpoints: whether or not actually draw the endpoints. Defaults to True. :type draw_endpoints: bool :param label_endpoints: whether or not label the endpoints. Defaults to True. :type label_endpoints: bool :param mark: the mark to print on the line segment :type mark: str :param mark_scale: the scale (size) of the mark. Defaults to 0.5 :type mark_scale: any number :param locked_label: to allow or prevent, by default, modifications of the LineSegment's label. :type locked_label: bool """ if len(points) != 2: raise TypeError('Two Points are required to create a ' 'LineSegment. Got {} object(s) instead.'.format( len(points))) Bipoint.__init__(self, *points, allow_zero_length=allow_zero_length) self._label = None self._thickness = None self._label_mask = None self._label_scale = None self._draw_endpoints = None self._label_endpoints = None self._locked_label = False # Only temporary, in order to be able to self.label = label # use the label setter. self.label_mask = label_mask self.label_scale = label_scale self.thickness = thickness self.dashpattern = dashpattern self.draw_endpoints = draw_endpoints self.sloped_label = sloped_label self.label_winding = label_winding self.label_endpoints = label_endpoints self.mark = mark self.mark_scale = mark_scale try: self.endpoints[1].label_position = \ tikz_approx_position(self.slope360) except ZeroLengthLineSegment: self.endpoints[1].label_position = 'below left' self.endpoints[0].label_position = \ OPPOSITE_LABEL_POSITIONS[self.endpoints[1].label_position] if label_position is None: label_position = 'automatic' self.label_position = label_position if color is not None: self.color = color self._comment_designation = 'Line Segment' if not isinstance(locked_label, bool): raise TypeError('Expected bool type for \'locked_label\' keyword ' 'argument. Found {}.'.format(type(locked_label))) self._locked_label = locked_label
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 __init__(self, start_vertex=None, name=None, side_length=Number('1'), mark_right_angles=True, 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 Square :param start_vertex: the vertex to start to draw the Square (default (0; 0)) :type start_vertex: Point :param name: the name of the Square, like ABCDE for a pentagon. 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 Square :type side_length: a number :param mark_right_angles: if True (default), all four angles will be automatically marked as right angles. :type mark_right_angles: bool :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 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 """ Rectangle.__init__(self, start_vertex=start_vertex, name=name, width=side_length, length=side_length, mark_right_angles=mark_right_angles, draw_vertices=draw_vertices, label_vertices=label_vertices, thickness=thickness, color=color, rotation_angle=rotation_angle, winding=winding, sloped_sides_labels=sloped_sides_labels) # TODO: see issue #5 # Accepted type for side_length is number, already checked at # vertices' instanciations (in Rectangle.__init__() call). self._side_length = Number(side_length) Equilateral.__init__(self, mark_equal_sides=mark_equal_sides, use_mark=use_mark) self._type = 'Square'
def __init__(self, x=None, y=None, z='undefined', name='automatic', color=None, shape=r'$\times$', shape_scale=Number('0.67'), label='default', label_position='below'): r""" Initialize Point When not naming the keyword arguments, it is possible to not mention the applicate, z: Point(3, 2, 'A') is almost equivalent to Point(3, 2, 0, 'A'): - all subsequent keyword arguments must be named - the first version is a 2D Point, the second version a 3D Point. :param x: the Point's abscissa :type x: anything that can be turned to a Number :param y: the Point's ordinate :type y: anything that can be turned to a Number :param z: the Point's applicate :type z: anything that can be turned to a Number :param name: the Point's name (e.g. 'A'). If it's left to 'automatic', a yet unused name will be set. :type name: str :param shape: the symbol that will be drawn at the Point's position. Default value is '$\times$', what draws a cross. :type shape: str :param label: what will be placed near the Point. The default value will set the Point's name as label. Setting label at '' will disable labeling the Point. :type label: str :param label_position: if any label is to be drawn, this is where to draw it. Available values are TikZ's ones ('above', 'below left'...). :type label_position: str """ self._three_dimensional = False self._name = None self._x = None self._y = None self._z = None self._shape = None self._label = None self._label_position = None self.x = x self.y = y try: self.z = z except (TypeError, InvalidOperation): # The third value cannot be used as z; let's assume it is actually # 2D geometry and the value is actually simply the name, implying # z should be set to 0. self.z = Number(0) self._three_dimensional = False self.name = z else: self.name = name self.shape = shape if label is 'default': self.label = self.name else: self.label = label self.label_position = label_position if color is not None: self.color = color self.shape_scale = shape_scale
def label_scale(self, value): if value is None: self._label_scale = None else: self._label_scale = Number(value)