Beispiel #1
0
 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')))
Beispiel #2
0
 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
Beispiel #3
0
 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))
Beispiel #5
0
 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)
Beispiel #6
0
 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
Beispiel #7
0
 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))))
Beispiel #9
0
 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()
Beispiel #10
0
 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)
Beispiel #11
0
 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'
Beispiel #12
0
 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
Beispiel #13
0
 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
Beispiel #14
0
 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
Beispiel #15
0
 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
Beispiel #16
0
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'
Beispiel #17
0
    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)
Beispiel #18
0
    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)
Beispiel #19
0
 def mark_scale(self, value):
     check_scale(value, 'LineSegment\'s mark')
     self._mark_scale = Number(value)
Beispiel #20
0
 def __init__(self):
     self.DEFAULT_ARMSPOINTS_POSITION = Number('0.8')
Beispiel #21
0
 def shape_scale(self, value):
     check_scale(value, 'Point\'s shape')
     self._shape_scale = Number(value)
Beispiel #22
0
 def x(self, value):
     try:
         self._x = Number(value)
     except (TypeError, InvalidOperation):
         raise TypeError('Expected a number as abscissa, found {} '
                         'instead.'.format(repr(value)))
Beispiel #23
0
 def y(self, value):
     try:
         self._y = Number(value)
     except (TypeError, InvalidOperation):
         raise TypeError('Expected a number as ordinate, found {} '
                         'instead.'.format(repr(value)))
Beispiel #24
0
    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
Beispiel #25
0
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 + '}')
Beispiel #26
0
    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
Beispiel #27
0
    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)
Beispiel #28
0
    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'
Beispiel #29
0
    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
Beispiel #30
0
 def label_scale(self, value):
     if value is None:
         self._label_scale = None
     else:
         self._label_scale = Number(value)