Repository: qthequartermasterman/geometry

Issue:
Implement Basic Geometric Constructions such as "Perpendicular Bisector" or "Midpoint"
Euclid very often uses foundational constructions such as "perpendicular bisector" or "midpoint" from Book I in later constructions and proofs. There should be methods in `Construction` class that automatically do these shortcut constructions. They should, however, mark these intermediate steps as "uninteresting" by setting `interesting=False` when adding the line or circle.

- [x] Euclid I2: Copy a segment (i.e. find the point on a given line that is the same distance from a given point as a given segment)
- [x] Euclid I3: Cut off a shorter segment
- [x] Euclid I9: Bisect an angle
- [x] Euclid I10: Midpoint between two points
- [x] Euclid I11: Erect a Perpendicular
- [x] Euclid I12: Drop a Perpendicular
- [x] Euclid I31: Parallel Lines

Discussion:

Patch:
diff --git a/geompy/__init__.py b/geompy/__init__.py
index e101ed1..60022de 100644
--- a/geompy/__init__.py
+++ b/geompy/__init__.py
@@ -7,12 +7,14 @@ if USE_EXACT:
     from geompy.core.Line import Line
     from geompy.core.Circle import Circle
     from geompy.core.Construction import Construction
+    from geompy.core.Angle import Angle
 else:
     from geompy.core.Object import Object
     from geompy.core.Point import FastPoint as Point
     from geompy.core.Line import FastLine as Line
     from geompy.core.Circle import FastCircle as Circle
     from geompy.core.Construction import Construction
+    from geompy.core.Angle import Angle
 
 from geompy.core import *
 
diff --git a/geompy/cas/__init__.py b/geompy/cas/__init__.py
index 479044d..5e440e0 100644
--- a/geompy/cas/__init__.py
+++ b/geompy/cas/__init__.py
@@ -31,4 +31,22 @@ else:
                                   is_nan,
                                   full_simplify)
 
-alphabet = list(map(chr, range(97, 123)))
+# alphabet = list(map(chr, range(97, 123)))
+
+
+def alphabet(index: int):
+    if index < 0:
+        raise ValueError(f'Cannot find letter with index {index}. Index is too small.')
+    elif index == 0:
+        return 'a'
+    list_of_remainders = []
+    while index:
+        # Keep dividing by 26. The remainder is the next letter. The index goes to the next round.
+        index, remainder = divmod(index, 26)
+        list_of_remainders.append(remainder)
+    if len(list_of_remainders) > 1:
+        list_of_remainders[-1] -= 1
+    string = ''.join(map(lambda x: chr(97+x), list_of_remainders))[::-1]  # Return reversed string
+    return string
+
+
diff --git a/geompy/core/Angle.py b/geompy/core/Angle.py
new file mode 100644
index 0000000..e2cf68b
--- /dev/null
+++ b/geompy/core/Angle.py
@@ -0,0 +1,32 @@
+from .Object import Object
+from .Line import Line
+from .Construction import Construction
+from .Point import Point
+from geompy.cas import equals
+from math import acos
+
+
+class Angle(Object):
+    def __init__(self, line1: Line, line2: Line):
+        """
+        Create a new angle. Note, the "interior" of the angle is oriented between the two rays pointing in the
+        directions of line1's and line2's direction vectors.
+        :param line1:
+        :param line2:
+        """
+        super().__init__()
+        self.line1 = line1
+        self.line2 = line2
+        (self.vertex_point,) = Construction.find_intersections_line_line(line1, line2)
+        self.dependencies = {line1, line2}
+
+    @property
+    def measure(self):
+        """Return the measure of the angle in radians."""
+        return acos(float(self.line1.get_direction_vector() * self.line2.get_direction_vector()))
+
+    def __eq__(self, other):
+        return isinstance(other, Angle) and equals(self.measure, other.measure)
+
+    def __repr__(self):
+        return f'Angle {self.vertex_point} formed by lines {self.line1} and {self.line2}'
diff --git a/geompy/core/Construction.py b/geompy/core/Construction.py
index 35845ab..0e5742d 100644
--- a/geompy/core/Construction.py
+++ b/geompy/core/Construction.py
@@ -107,7 +107,7 @@ class Construction:
                 intersect.dependencies.update({object1, object2})
                 if not intersect.name:
                     # intersect.name = f'"{i}"'
-                    intersect.name = alphabet[i]
+                    intersect.name = alphabet(i)
                 i += 1
             self.points.update(intersections)
             if interesting:
@@ -404,9 +404,12 @@ class Construction:
         :param interesting: Should the point be marked as interesting?
         :return: the point we added
         """
-        self.points.add(point)
-        if interesting:
-            self.interesting_points.add(point)
+        if point not in self.points:
+            if not point.name:
+                point.name = alphabet(len(self.points))
+            self.points.add(point)
+            if interesting:
+                self.interesting_points.add(point)
         self.actions = self.get_valid_actions({point})
         return point
 
diff --git a/geompy/core/EuclidConstructions.py b/geompy/core/EuclidConstructions.py
index cce57df..176fe64 100644
--- a/geompy/core/EuclidConstructions.py
+++ b/geompy/core/EuclidConstructions.py
@@ -1,5 +1,5 @@
 """This file contains various constructions found in Euclid's Elements"""
-from geompy import Point, Construction, Line
+from geompy import Point, Construction, Line, Angle
 from geompy.core.Construction import ConstructionMode
 from geompy.cas import Infinity
 
@@ -37,6 +37,14 @@ def pick_point_not_on_line(line: Line):
     """Pick a point not on the line for when we do not care about which side."""
     return line.point1 + line.get_perpendicular_at_point(line.point1).get_direction_vector()
 
+def pick_point_not_on_line_on_side(line: Line, side: Point, same_side=True):
+    """Pick a point that is not on the line, but we do care which side."""
+    point2 = line.point1 + line.get_perpendicular_at_point(line.point1).get_direction_vector()
+    if check_if_points_on_same_side(line, side, point2) == same_side:
+        return point2
+    else:
+        return line.point1 - line.get_perpendicular_at_point(line.point1).get_direction_vector()
+
 
 def BaseConstruction(name='', construction_mode=ConstructionMode.DEFAULT):
     """A construction with two points a unit length apart.
@@ -70,16 +78,14 @@ def EuclidI1(construction: Construction, line: Line, side: Point, interesting=Tr
     An extra point is required to specify which side of the line to construct the triangle. Euclid takes for granted
     that it can be done on either side, and does so on the top, without loss of generality.
 
-    :param construction:
-    :param line:
-    :param side:
+    :param construction: construction in which to do the work
+    :param line: line upon which to construct the triangle. line's point1 and point2 should be vertices of triangle
+    :param side: any point on the same side of line as the desired vertex
     :param interesting: bool representing whether or not to mark subsequent steps as interesting.
-    :return:
+    :return: the point representing the third vertex of the equilateral triangle
     """
     if side in line:
         raise ValueError(f'Point {side} is on line {line}, so side is ambiguous when erecting triangle.')
-    if line not in construction.lines:
-        raise ValueError(f'Cannot erect triangle. {line} is not in {construction}')
 
     a, b = line.point1, line.point2
     # These are the steps of the constructions
@@ -99,7 +105,18 @@ def EuclidI1(construction: Construction, line: Line, side: Point, interesting=Tr
         raise ValueError(f'No intersections were on the same side of {line} as point {side}')
 
 
+ErectEquilateralTriangle = EuclidI1
+
+
 def EuclidI2(construction: Construction, line_segment: Line, a: Point, interesting=True) -> Line:
+    """
+    To copy a segment.
+    :param construction: construction in which to do the work
+    :param line_segment: Line segment to be copied
+    :param a: the first point of the new, copied line segment
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: a new line whose point1 is a and point2 is the distance of line_segment away from a
+    """
     if a not in construction.points:
         raise ValueError(f'Cannot copy line segment. Point {a} not in {construction}.')
     # Choose b,c as the ends of the linesegments.
@@ -117,10 +134,21 @@ def EuclidI2(construction: Construction, line_segment: Line, a: Point, interesti
     circle_dg = construction.add_circle(d, g, interesting=interesting)
     intersections = construction.find_intersections_line_circle(Line(d, a), circle_dg)
     final_point = pick_point_on_side(ab, d, intersections, same_side=False)
-    return Line(a, final_point)
+    return construction.add_line(a, final_point, interesting=interesting)
+
+
+CopySegment = EuclidI2
 
 
 def EuclidI3(construction: Construction, short_line: Line, long_line: Line, interesting=True) -> Line:
+    """
+    To cut off a segment.
+    :param construction: construction in which to do the work
+    :param short_line: the line whose point1 and point2 are the desired length apart
+    :param long_line: the longer line from which we will cut the distance of short_line off
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: line equal to long_line, but whose point2 is a cut off to the appropriate length
+    """
     if short_line not in construction.lines or long_line not in construction.lines:
         raise ValueError(f'Cannot cut off line segment. {short_line} or {long_line} not in {construction}')
     a, b = long_line.point1, long_line.point2
@@ -132,6 +160,174 @@ def EuclidI3(construction: Construction, short_line: Line, long_line: Line, inte
     return Line(a, e)
 
 
+CutOffSegment = EuclidI3
+
+
+def EuclidI9(construction: Construction, angle: Angle, interesting=True) -> Line:
+    """
+    To bisect an angle.
+    :param construction: construction in which to do the work
+    :param angle: angle from which to bisect
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the line that bisects angle
+    """
+    a = angle.vertex_point
+    line1, line2 = angle.line1, angle.line2
+    d = line1.point1 if line1.point1 != a else line1.point2  # pick an arbitrary point on line1 that is not a.
+    # Cut off point E from line2 with length AD
+    e = EuclidI3(construction, short_line=Line(a, d), long_line=line2, interesting=interesting).point2
+    # We need to pick a point opposite DE from A to show which side to erect the equilateral triangle.
+    # Start at D, and walk in the direction of D-A.
+    side = 2 * d - a
+    line_de = construction.add_line(d, e, interesting=interesting)
+    f = EuclidI1(construction, line_de, side, interesting=interesting)
+    return construction.add_line(a, f, interesting=interesting)
+
+
+AngleBisector = EuclidI9
+
+
+def EuclidI10(construction: Construction, line: Line, interesting=True) -> Point:
+    """
+    To bisect a given finite straight line.
+
+    NOTE: This construction is included in here as written in Elements for completion sake. Erecting a triangle and
+    then bisecting the angle is effective, but slow when drawing every step. Use the perpendicular bisector instead.
+    This construction is given first to make the proof easier.
+    :param construction: construction in which to do the work
+    :param line: line segment to be bisected
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the midpoint of the line
+    """
+    a, b = line.point1, line.point2
+    side = pick_point_not_on_line(line)  # We don't care which side to erect the triangle.
+    c = EuclidI1(construction, line, side, interesting=interesting)
+    angle = Angle(Line(c, b), Line(c, a))
+    perp_bisector = EuclidI9(construction, angle, interesting=interesting)
+    (intersections,) = construction.find_intersections_line_line(line, perp_bisector)
+    return intersections
+
+
+Midpoint = EuclidI10
+
+
+def PerpendicularBisector(construction: Construction, line: Line, interesting=True) -> Line:
+    """
+    Erect the perpendicular bisector of a given line much faster.
+    :param construction: construction in which to do the work
+    :param line: line segment to be bisected
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the perpendicular bisector of line
+    """
+    a, b = line.point1, line.point2
+    # These are the steps of the constructions
+    circ1 = construction.add_circle(a, b, interesting=interesting)
+    circ2 = construction.add_circle(b, a, interesting=interesting)
+    intersections = construction.find_intersections(circ1, circ2)  # Only include circle intersections.
+    return construction.add_line(*intersections, interesting=interesting)
+
+
+def EuclidI11(construction: Construction, line: Line, point: Point, interesting=True) -> Line:
+    """
+    To draw a straight line at right angles to a given straight line from a given point on it.
+    "To erect the perpendicular"
+
+    :param construction: construction in which to do the work
+    :param line: the line of which the perpendicular will be erected
+    :param point: point on line through which perpendicular should pass
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the perendicular to line passing through point
+    """
+    if point not in line:
+        raise ValueError(f'Cannot erect a perpendicular. Point {point} is not on line {line}.')
+    # rename point as c
+    c = point
+    # Pick an arbitrary point D on the line.
+    d = line.point1 if line.point1 != point else line.point2
+    # Make CE equal to CD.
+    cCrCD = construction.add_circle(c, d, interesting=interesting)
+    intersections = list(construction.find_intersections_line_circle(line, cCrCD))
+    e = intersections[0] if intersections[0] != d else intersections[1]
+    # Construct the equilateral triangle FDE on DE
+    f = EuclidI1(construction, Line(d, e), pick_point_not_on_line(line))
+    # join CF
+    return construction.add_line(c, f, interesting=interesting)
+
+
+ErectPerpendicular = EuclidI11
+
+def EuclidI12(construction, line: Line, point: Point, interesting=True) -> Line:
+    """
+    To draw a straight line perpendicular to a given infinite straight line from a given point not on it.
+    "To drop a perpendicular"
+
+    :param construction: construction in which to do the work
+    :param line: the line of which the perpendicular will be erected
+    :param point: point not on line through which perpendicular should pass
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the perendicular to line passing through point
+    """
+    if point in line:
+        raise ValueError(f'Cannot drop a perpendicular. Point {point} is on line {line}.')
+    # rename point as C
+    c = point
+    d = pick_point_not_on_line_on_side(line, c, same_side=False)  # Pick a point on the opposite side of line from c
+    cCrCD = construction.add_circle(c, d, interesting=interesting)
+    a, b = construction.find_intersections_line_circle(line, cCrCD)
+    side = d
+    f = ErectEquilateralTriangle(construction, Line(a, b), side=side, interesting=interesting)
+    return construction.add_line(c, f)
+
+DropPerpendicular = EuclidI12
+
+def Perpendicular(construction, line: Line, point: Point, interesting=True) -> Line:
+    """
+    Perpendicular to line through point. Will choose the proper construction depending on if point is on the line or not
+
+    :param construction: construction in which to do the work
+    :param line: the line of which the perpendicular will be erected
+    :param point: point through which perpendicular should pass
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the perendicular to line passing through point
+    """
+    if point in line:
+        return ErectPerpendicular(construction, line, point, interesting=interesting)
+    else:
+        return DropPerpendicular(construction, line, point, interesting=interesting)
+
+
+
+def EuclidI31(construction, line: Line, point: Point, interesting=True) -> Line:
+    """
+    To draw a straight line through a given point parallel to a given straight line.
+
+    :param construction: construction in which to do the work
+    :param line: the original line, whose parallel we will find
+    :param point: point through which the parallel should pass
+    :param interesting: bool representing whether or not to mark subsequent steps as interesting.
+    :return: the parallel to line passing through point
+    """
+    if point in line:
+        # If the point is already on the line, the parallel of the line is the line itself
+        return line
+    # rename point as A, points B,C on line
+    a = point
+    b, c = line.point1, line.point2
+    cBrAB = construction.add_circle(b, a, interesting=interesting)
+    intersections = construction.find_intersections_line_circle(line, cBrAB)
+    d = list(intersections)[0]  # It doesn't matter which one we pick.
+    cDrAD = construction.add_circle(d, a, interesting=interesting)
+    intersections = list(construction.find_intersections_circle_circle(cBrAB, cDrAD))
+    e = intersections[0] if intersections[0] != a else intersections[1]  # Pick the intersection not A
+    de = construction.add_line(d, e, interesting=interesting)
+    intersections = list(construction.find_intersections_line_circle(de, cDrAD))
+    f = intersections[0] if intersections[0] != e else intersections[1]
+    return construction.add_line(a, f, interesting=interesting)
+
+
+ParallelLine = EuclidI31
+
+
 def RandomConstruction(length, construction_mode=ConstructionMode.DEFAULT):
     """Generates a random construction of given length. Not included in Euclid, but sometimes useful, nonetheless."""
     construction = BaseConstruction(name=f'RandomConstruction_{length}', construction_mode=construction_mode)
diff --git a/geompy/core/Line.py b/geompy/core/Line.py
index 0e1562b..cde007d 100644
--- a/geompy/core/Line.py
+++ b/geompy/core/Line.py
@@ -23,7 +23,7 @@ class Line(Object):
         :param name: optional string representing the name of the Line
         """
         super().__init__()
-        if point1 == point2:
+        if point1 == point2 and (slope is None or intercept is None):
             raise ValueError(f'Line cannot be uniquely defined from one point: point1={point1} and point2={point2}')
         if slope is None and intercept is None:
             self.point1 = point1
@@ -34,8 +34,9 @@ class Line(Object):
         else:
             self.slope = slope
             self.intercept = intercept
-            self.point1 = Point(0, intercept)
-            self.point2 = Point(1, slope + intercept)
+            if slope != Infinity:
+                self.point1 = Point(0, intercept)
+                self.point2 = Point(1, slope + intercept)
         # self.name = name if name else u'\u0305'.join(f'{point1.name}{point2.name} ')
         self.name = name if name else f'{point1.name}{point2.name}'
         self._simplified = pre_simplified


Test patch:
diff --git a/tests/test_core/test_Angle.py b/tests/test_core/test_Angle.py
new file mode 100644
index 0000000..846e1fa
--- /dev/null
+++ b/tests/test_core/test_Angle.py
@@ -0,0 +1,81 @@
+from .test_constants import GeometryTestCase
+
+from geompy import Angle, Line, Point
+from geompy.core.EuclidConstructions import EquilateralUnitTriangle
+from math import pi
+
+
+class AngleTest(GeometryTestCase):
+    def test_angle_measure(self):
+        """
+            c
+          / |
+         /  |
+        a---b
+        """
+        a = Point(0, 0)
+        b = Point(1, 0)
+        c = Point(1, 1)
+        line_ab = Line(a, b)
+        line_ac = Line(a, c)
+        angle_abc = Angle(line_ab, line_ac)
+        self.assertAlmostEqual(pi / 4, angle_abc.measure)
+
+    def test_angle_init(self):
+        """
+            c
+          / |
+         /  |
+        a---b
+        """
+        a = Point(0, 0)
+        b = Point(1, 0)
+        c = Point(1, 1)
+        line_ab = Line(a, b)
+        line_ac = Line(a, c)
+        angle_abc = Angle(line_ab, line_ac)
+        self.assertEqual(a, angle_abc.vertex_point)
+
+    def test_angle_equality(self):
+        """
+                    c
+                  / |
+                 /  |
+                a---b
+                """
+
+        a = Point(0, 0)
+        b = Point(1, 0)
+        c = Point('1/2', 'sqrt(3)/2')
+        # Note, that we double up these line definitions so we can orient all of our angles inwards.
+        line_ab, line_ba = Line(a, b), Line(b, a)
+        line_ac, line_ca = Line(a, c), Line(c, a)
+        line_bc, line_cb = Line(b, c), Line(c, b)
+        angle_cab = Angle(line_ac, line_ab)
+        angle_cba = Angle(line_bc, line_ba)
+        angle_acb = Angle(line_ca, line_cb)
+
+        self.assertEqual(angle_acb, angle_cab)
+        self.assertEqual(angle_acb, angle_cba)
+
+    def test_supplementary_angles(self):
+        a = Point(0, 0)
+        b = Point(1, 0)
+        c = Point(1, 1)
+        line_ab, line_ba = Line(a, b), Line(b, a)
+        line_ac, line_ca = Line(a, c), Line(c, a)
+        angle_cab = Angle(line_ac, line_ab)
+        angle_bacprime = Angle(line_ab, line_ca)
+        angle_cabprime = Angle(line_ac, line_ba)
+        self.assertEqual(pi, angle_cab.measure+angle_bacprime.measure)
+        self.assertEqual(pi, angle_cab.measure+angle_cabprime.measure)
+
+    def test_opposite_interior_angles(self):
+        a = Point(0, 0)
+        b = Point(1, 0)
+        c = Point(1, 1)
+        line_ab, line_ba = Line(a, b), Line(b, a)
+        line_ac, line_ca = Line(a, c), Line(c, a)
+        angle_cab = Angle(line_ac, line_ab)
+        angle_opposite = Angle(line_ca, line_ba)
+        self.assertEqual(angle_opposite, angle_cab)
\ No newline at end of file
diff --git a/tests/test_core/test_EuclidConstructions.py b/tests/test_core/test_EuclidConstructions.py
index b9cd744..4f299b0 100644
--- a/tests/test_core/test_EuclidConstructions.py
+++ b/tests/test_core/test_EuclidConstructions.py
@@ -1,8 +1,10 @@
-from unittest import TestCase
+from unittest import TestCase, skip
 
-from geompy import Point, Line
+from geompy import Point, Line, Angle
 from geompy.core.EuclidConstructions import (check_if_points_on_same_side, EquilateralUnitTriangle, BaseConstruction,
-                                             EuclidI2, EuclidI3, RandomConstruction)
+                                             EuclidI2, EuclidI3, RandomConstruction, EuclidI9, EuclidI10, Midpoint,
+                                             PerpendicularBisector, EuclidI11, EuclidI12, ParallelLine, Perpendicular,
+                                             DropPerpendicular, ErectPerpendicular)
 
 
 class Test(TestCase):
@@ -41,13 +43,109 @@ class Test(TestCase):
     def test_EuclidI3(self):
         construction = BaseConstruction()
         a, b = construction.points
-        c = construction.add_point(Point(10, 10))
+        c = construction.add_point(Point(10, 10, name='C'))
         short_line = construction.add_line(a, b)
         long_line = construction.add_line(a, c)
         shortened_line = EuclidI3(construction, short_line=short_line, long_line=long_line)
         self.assertEqual(abs(Line(a, b)), abs(shortened_line))
 
+    @skip
+    def test_EuclidI9(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        c = construction.add_point(Point(1, 1, name='C'))
+        line_ab = construction.add_line(a, b)
+        line_ac = construction.add_line(a, c)
+        angle_abc = Angle(line_ab, line_ac)
+        line_bisecting_angle_abc = EuclidI9(construction, angle_abc)
+        # Test if the line is the angle bisector by testing if the two sub angles are equal
+        self.assertEqual(Angle(line_bisecting_angle_abc, line_ab), Angle(line_bisecting_angle_abc, line_ab))
+
+    def test_EuclidI10(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        line_ab = construction.add_line(a, b)
+        midpoint = EuclidI10(construction, line_ab)
+        self.assertEqual(Point('1/2', 0), midpoint)
+        midpoint = Midpoint(construction, line_ab)
+        self.assertEqual(Point('1/2', 0), midpoint)
+
+    def test_PerpendicularBisector(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        line_ab = construction.add_line(a, b)
+        bisector = PerpendicularBisector(construction, line_ab)
+        self.assertEqual(Line(Point('1/2',0), Point('1/2',1)), bisector)
+
+    def test_ErectPerpendicular(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        line_ab = construction.add_line(a, b)
+        bisector = EuclidI11(construction, line_ab, Point('1/2', 0))
+        self.assertEqual(Line(Point('1/2', 0), Point('1/2', 1)), bisector)
+
+    def test_ErectPerpendicularPointNotOnLineFails(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        c = construction.add_point(Point(1, 10, name='C'))
+        self.assertRaises(ValueError, lambda: ErectPerpendicular(construction, Line(a,b), c))
+
+
+    def test_DropPerpendicular(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        line_ab = construction.add_line(a, b)
+        bisector = EuclidI12(construction, line_ab, Point('1/2', 1))
+        self.assertEqual(Line(Point('1/2', 0), Point('1/2', 1)), bisector)
+
+    def test_DropPerpendicularPointOnLineFails(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        c = construction.add_point(Point(10, 0, name='C'))
+        self.assertRaises(ValueError, lambda: DropPerpendicular(construction, Line(a, b), c))
+
+    def test_Perpendicular(self):
+        construction = BaseConstruction()
+        a, b = construction.points
+        if a.name != 'A':
+            # Swap the points if we grabbed them backwards
+            a, b = b, a
+        c = construction.add_point(Point(1, 10, name='C'))
+        perpendicular = Perpendicular(construction, Line(a,b), c)
+        self.assertEqual(Line(Point(1,0), Point(1,10)), perpendicular)
+        d = construction.add_point(Point(0, 10, name='D'))
+        perpendicular = Perpendicular(construction, Line(a, b), d)
+        self.assertEqual(Line(Point(0, 0), Point(0, 10)), perpendicular)
+
     def test_RandomConstruction(self):
         for i in range(5):
             construction = RandomConstruction(length=i)
             self.assertEqual(i, len(construction))
+
+    def test_Parallel(self):
+        construction = BaseConstruction()
+        a = Point(1,1)
+        parallel = ParallelLine(construction, Line(*construction.points), a)
+        self.assertEqual(0, parallel.slope)
+        self.assertEqual(1, parallel.intercept)