From f58fe4df0da454bc69b70589d1111aefaa36a182 Mon Sep 17 00:00:00 2001
From: Alan Odom <clockmender@icloud.com>
Date: Sun, 16 Feb 2020 19:57:38 +0000
Subject: [PATCH] PDT: Further Expansion of Tangent System

This is still WIP.

Further expand options to work in any plane and from selected vertices.
DocStrings Added. Code refactored.

Further testing is still required before this can be released for general use.
---
 pdt_tangent.py                             | 666 ++++++++++++++++-----
 precision_drawing_tools/__init__.py        |   7 +
 precision_drawing_tools/pdt_exception.py   |   4 +
 precision_drawing_tools/pdt_functions.py   |   2 +
 precision_drawing_tools/pdt_menus.py       |  58 +-
 precision_drawing_tools/pdt_msg_strings.py |   3 +
 6 files changed, 583 insertions(+), 157 deletions(-)

diff --git a/pdt_tangent.py b/pdt_tangent.py
index 1f2a7ab93..a00be3138 100644
--- a/pdt_tangent.py
+++ b/pdt_tangent.py
@@ -23,96 +23,371 @@
 #
 import bpy
 import bmesh
-from math import sqrt
+from math import sqrt, floor, asin, sin, cos, pi
 from mathutils import Vector
 from bpy.types import Operator
 
 from .pdt_functions import (
     oops,
     arc_centre,
+    set_mode,
+    view_coords,
+    view_coords_i,
 )
 
 from .pdt_msg_strings import (
     PDT_OBJ_MODE_ERROR,
     PDT_ERR_NO_ACT_OBJ,
     PDT_ERR_SEL_3_VERTS,
+    PDT_ERR_SEL_1_VERT,
+    PDT_ERR_BADDISTANCE,
+    PDT_ERR_MATHSERROR,
+    PDT_ERR_SAMERADII,
+    PDT_ERR_VERT_MODE,
 )
 
 from . import pdt_exception
 PDT_ObjectModeError = pdt_exception.ObjectModeError
-PDT_NoObjectError = pdt_exception.NoObjectError
 PDT_SelectionError = pdt_exception.SelectionError
 
 
-def get_tangent_intersect_outer(xloc_0, yloc_0, xloc_1, yloc_1, radius_0, radius_1):
-    xloc_p = ((xloc_1 * radius_0) - (xloc_0 * radius_1)) / (radius_0 - radius_1)
-    yloc_p = ((yloc_1 * radius_0) - (yloc_0 * radius_1)) / (radius_0 - radius_1)
+def get_tangent_intersect_outer(hloc_0, vloc_0, hloc_1, vloc_1, radius_0, radius_1):
+    """Return Location in 2 Dimensions of the Intersect Point for Outer Tangents.
 
-    return xloc_p, yloc_p
+    Args:
+        hloc_0: Horizontal Coordinate of Centre of First Arc
+        vloc_0: Vertical Coordinate of Centre of First Arc
+        hloc_1: Horizontal Coordinate of Centre of Second Arc
+        vloc_1: Vertical Coordinate of Centre of Second Arc
+        radius_0: Radius of First Arc
+        radius_1: Radius of Second Arc
 
+    Returns:
+        hloc_p: Horizontal Coordinate of Centre of Intersection
+        vloc_p: Vertical Coordinate of Centre of Intersection.
+    """
 
-def get_tangent_intersect_inner(xloc_0, yloc_0, xloc_1, yloc_1, radius_0, radius_1):
-    xloc_p = ((xloc_1 * radius_0) + (xloc_0 * radius_1)) / (radius_0 + radius_1)
-    yloc_p = ((yloc_1 * radius_0) + (yloc_0 * radius_1)) / (radius_0 + radius_1)
+    hloc_p = ((hloc_1 * radius_0) - (hloc_0 * radius_1)) / (radius_0 - radius_1)
+    vloc_p = ((vloc_1 * radius_0) - (vloc_0 * radius_1)) / (radius_0 - radius_1)
 
-    return xloc_p, yloc_p
+    return hloc_p, vloc_p
 
 
-def get_tangent_points(xloc_0, yloc_0, radius_0, xloc_p, yloc_p):
-    numerator = (radius_0 ** 2 * (xloc_p - xloc_0)) + (
+def get_tangent_intersect_inner(hloc_0, vloc_0, hloc_1, vloc_1, radius_0, radius_1):
+    """Return Location in 2 Dimensions of the Intersect Point for Inner Tangents.
+
+    Args:
+        hloc_0: Horizontal Coordinate of Centre of First Arc
+        vloc_0: Vertical Coordinate of Centre of First Arc
+        hloc_1: Horizontal Coordinate of Centre of Second Arc
+        vloc_1: Vertical Coordinate of Centre of Second Arc
+        radius_0: Radius of First Arc
+        radius_1: Radius of Second Arc
+
+    Returns:
+        hloc_p: Horizontal Coordinate of Centre of Intersection
+        vloc_p: Vertical Coordinate of Centre of Intersection.
+    """
+
+    hloc_p = ((hloc_1 * radius_0) + (hloc_0 * radius_1)) / (radius_0 + radius_1)
+    vloc_p = ((vloc_1 * radius_0) + (vloc_0 * radius_1)) / (radius_0 + radius_1)
+
+    return hloc_p, vloc_p
+
+
+def get_tangent_points(context, hloc_0, vloc_0, radius_0, hloc_p, vloc_p):
+    """Return Location in 2 Dimensions of the Tangent Points.
+
+    Args:
+        context: Blender bpy.context instance
+        hloc_0: Horizontal Coordinate of Centre of First Arc
+        vloc_0: Vertical Coordinate of Centre of First Arc
+        radius_0: Radius of First Arc
+        hloc_p: Horizontal Coordinate of Intersection
+        vloc_p: Vertical Coordinate of Intersection
+
+    Returns:
+        hloc_t1: Horizontal Location of First Tangent Point
+        hloc_t2: Horizontal Location of Second Tangent Point
+        vloc_t1: Vertical Location of First Tangent Point
+        vloc_t2: Vertical Location of Second Tangent Point
+    """
+    numerator = (radius_0 ** 2 * (hloc_p - hloc_0)) + (
         radius_0
-        * (yloc_p - yloc_0)
-        * sqrt((xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2 - radius_0 ** 2)
+        * (vloc_p - vloc_0)
+        * sqrt((hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2 - radius_0 ** 2)
     )
-    denominator = (xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2
-    xloc_t1 = round((numerator / denominator) + xloc_0, 5)
+    denominator = (hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2
+    hloc_t1 = round((numerator / denominator) + hloc_0, 5)
 
-    numerator = (radius_0 ** 2 * (xloc_p - xloc_0)) - (
+    numerator = (radius_0 ** 2 * (hloc_p - hloc_0)) - (
         radius_0
-        * (yloc_p - yloc_0)
-        * sqrt((xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2 - radius_0 ** 2)
+        * (vloc_p - vloc_0)
+        * sqrt((hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2 - radius_0 ** 2)
     )
-    denominator = (xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2
-    xloc_t2 = round((numerator / denominator) + xloc_0, 5)
+    denominator = (hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2
+    hloc_t2 = round((numerator / denominator) + hloc_0, 5)
 
     # Get Y values
-    numerator = (radius_0 ** 2 * (yloc_p - yloc_0)) - (
+    numerator = (radius_0 ** 2 * (vloc_p - vloc_0)) - (
         radius_0
-        * (xloc_p - xloc_0)
-        * sqrt((xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2 - radius_0 ** 2)
+        * (hloc_p - hloc_0)
+        * sqrt((hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2 - radius_0 ** 2)
     )
-    denominator = (xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2
-    yloc_t1 = round((numerator / denominator) + yloc_0, 5)
+    denominator = (hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2
+    vloc_t1 = round((numerator / denominator) + vloc_0, 5)
 
-    numerator = (radius_0 ** 2 * (yloc_p - yloc_0)) + (
+    numerator = (radius_0 ** 2 * (vloc_p - vloc_0)) + (
         radius_0
-        * (xloc_p - xloc_0)
-        * sqrt((xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2 - radius_0 ** 2)
+        * (hloc_p - hloc_0)
+        * sqrt((hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2 - radius_0 ** 2)
     )
-    denominator = (xloc_p - xloc_0) ** 2 + (yloc_p - yloc_0) ** 2
-    yloc_t2 = round((numerator / denominator) + yloc_0, 5)
-
-    return xloc_t1, xloc_t2, yloc_t1, yloc_t2
-
-
-def draw_tangents(
-    xloc_to1, xloc_to2, yloc_to1, yloc_to2, xloc_to3, xloc_to4, yloc_to3, yloc_to4, bm, obj, obj_loc
-):
-    tangent_vector_o1 = Vector((xloc_to1, 0, yloc_to1))
-    tangent_vertex_o1 = bm.verts.new(tangent_vector_o1 - obj_loc)
-    tangent_vector_o2 = Vector((xloc_to2, 0, yloc_to2))
-    tangent_vertex_o2 = bm.verts.new(tangent_vector_o2 - obj_loc)
-    tangent_vector_o3 = Vector((xloc_to3, 0, yloc_to3))
-    tangent_vertex_o3 = bm.verts.new(tangent_vector_o3 - obj_loc)
-    tangent_vector_o4 = Vector((xloc_to4, 0, yloc_to4))
-    tangent_vertex_o4 = bm.verts.new(tangent_vector_o4 - obj_loc)
-    # Add Edges
-    bm.edges.new([tangent_vertex_o1, tangent_vertex_o3])
-    bm.edges.new([tangent_vertex_o2, tangent_vertex_o4])
+    denominator = (hloc_p - hloc_0) ** 2 + (vloc_p - vloc_0) ** 2
+    vloc_t2 = round((numerator / denominator) + vloc_0, 5)
+
+    return hloc_t1, hloc_t2, vloc_t1, vloc_t2
+
+
+def make_vectors(coords, a1, a2, a3, pg):
+    """Return Vectors of the Tangent Points.
+
+    Args:
+        coords: A List of Coordinates in 2D space of the tangent points
+                & a third dimension for the vectors
+        a1: Index of horizontal axis
+        a2: Index of vertical axis
+        a3: Index of depth axis
+        pg: PDT Parameters Group - our variables
+
+    Returns:
+        tangent_vector_o1: Location of First Tangent Point
+        tangent_vector_o2: Location of Second Tangent Point
+        tangent_vector_o3: Location of First Tangent Point
+        tangent_vector_o4: Location of Second Tangent Point
+    """
+
+    tangent_vector_o1 = Vector((0, 0, 0))
+    tangent_vector_o1[a1] = coords[0]
+    tangent_vector_o1[a2] = coords[1]
+    tangent_vector_o1[a3] = coords[8]
+    tangent_vector_o2 = Vector((0, 0, 0))
+    tangent_vector_o2[a1] = coords[2]
+    tangent_vector_o2[a2] = coords[3]
+    tangent_vector_o2[a3] = coords[8]
+    tangent_vector_o3 = Vector((0, 0, 0))
+    tangent_vector_o3[a1] = coords[4]
+    tangent_vector_o3[a2] = coords[5]
+    tangent_vector_o3[a3] = coords[8]
+    tangent_vector_o4 = Vector((0, 0, 0))
+    tangent_vector_o4[a1] = coords[6]
+    tangent_vector_o4[a2] = coords[7]
+    tangent_vector_o4[a3] = coords[8]
+
+    if pg.plane == "LO":
+        tangent_vector_o1 = view_coords(tangent_vector_o1[a1], tangent_vector_o1[a2],
+        tangent_vector_o1[a3])
+        tangent_vector_o2 = view_coords(tangent_vector_o2[a1], tangent_vector_o2[a2],
+        tangent_vector_o2[a3])
+        tangent_vector_o3 = view_coords(tangent_vector_o3[a1], tangent_vector_o3[a2],
+        tangent_vector_o3[a3])
+        tangent_vector_o4 = view_coords(tangent_vector_o4[a1], tangent_vector_o4[a2],
+        tangent_vector_o4[a3])
+
+    return ((tangent_vector_o1, tangent_vector_o2, tangent_vector_o3, tangent_vector_o4))
+
+def tangent_setup(context, pg, plane, obj_data, centre_0, centre_1, centre_2, radius_0, radius_1):
+    # Depth is a3
+    a1, a2, a3 = set_mode(plane)
+    if plane == "LO":
+        centre_0 = view_coords_i(centre_0[a1], centre_0[a2], centre_0[a3])
+        centre_1 = view_coords_i(centre_1[a1], centre_1[a2], centre_1[a3])
+        centre_2 = view_coords_i(centre_2[a1], centre_2[a2], centre_2[a3])
+    if pg.tangent_from_point:
+        vector_difference = centre_2 - centre_0
+        distance = sqrt(vector_difference[a1] ** 2 + vector_difference[a2] ** 2)
+    else:
+        vector_difference = centre_1 - centre_0
+        distance = sqrt(vector_difference[a1] ** 2 + vector_difference[a2] ** 2)
+    if distance > radius_0 + radius_1 and not pg.tangent_from_point:
+        mode = "inner"
+    elif distance > radius_0 and distance > radius_1 and not pg.tangent_from_point:
+        mode = "outer"
+    elif distance > radius_1 and pg.tangent_from_point:
+        mode = "point"
+    else:
+        # Cannot execute, centres are too close.
+        pg.error = f"{PDT_ERR_BADDISTANCE}"
+        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+        return {"FINISHED"}
+
+    if mode == "point":
+        if (
+            ((centre_2[a1] - centre_0[a1]) ** 2 +
+            (centre_2[a2] - centre_0[a2]) ** 2 -
+            radius_0 ** 2) > 0
+            ):
+            hloc_to1, hloc_to2, vloc_to1, vloc_to2 = get_tangent_points(context,
+                centre_0[a1], centre_0[a2], radius_0, centre_2[a1], centre_2[a2]
+            )
+        else:
+            pg.error = PDT_ERR_MATHSERROR
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+            return {"FINISHED"}
+        # Point Tangents
+        tangent_vector_o1 = Vector((0, 0, 0))
+        tangent_vector_o1[a1] = hloc_to1
+        tangent_vector_o1[a2] = vloc_to1
+        tangent_vector_o1[a3] = centre_2[a3]
+        tangent_vector_o2 = Vector((0, 0, 0))
+        tangent_vector_o2[a1] = hloc_to2
+        tangent_vector_o2[a2] = vloc_to2
+        tangent_vector_o2[a3] = centre_2[a3]
+        if pg.plane == "LO":
+            centre_2 = view_coords(centre_2[a1], centre_2[a2], centre_2[a3])
+            tangent_vector_o1 = view_coords(tangent_vector_o1[a1], tangent_vector_o1[a2],
+            tangent_vector_o1[a3])
+            tangent_vector_o2 = view_coords(tangent_vector_o2[a1], tangent_vector_o2[a2],
+            tangent_vector_o2[a3])
+        tangent_vectors = ((centre_2, tangent_vector_o1, tangent_vector_o2))
+        draw_tangents(tangent_vectors, obj_data)
+
+        return {"FINISHED"}
+
+    if mode in {"outer", "inner"}:
+        # Outer Tangents
+        if radius_0 == radius_1:
+            # No intersection point for outer tangents
+            sin_angle = (centre_1[a2] - centre_0[a2]) / distance
+            cos_angle = (centre_1[a1] - centre_0[a1]) / distance
+            hloc_to1 = centre_0[a1] + (radius_0 * sin_angle)
+            hloc_to2 = centre_0[a1] - (radius_0 * sin_angle)
+            hloc_to3 = centre_1[a1] + (radius_0 * sin_angle)
+            hloc_to4 = centre_1[a1] - (radius_0 * sin_angle)
+            vloc_to1 = centre_0[a2] - (radius_0 * cos_angle)
+            vloc_to2 = centre_0[a2] + (radius_0 * cos_angle)
+            vloc_to3 = centre_1[a2] - (radius_0 * cos_angle)
+            vloc_to4 = centre_1[a2] + (radius_0 * cos_angle)
+        else:
+            hloc_po, vloc_po = get_tangent_intersect_outer(
+                centre_0[a1], centre_0[a2], centre_1[a1], centre_1[a2], radius_0, radius_1
+            )
+
+            if (
+                ((hloc_po - centre_0[a1]) ** 2 +
+                (vloc_po - centre_0[a2]) ** 2 -
+                radius_0 ** 2) > 0
+                ):
+                hloc_to1, hloc_to2, vloc_to1, vloc_to2 = get_tangent_points(context,
+                    centre_0[a1], centre_0[a2], radius_0, hloc_po, vloc_po
+                )
+            else:
+                pg.error = PDT_ERR_MATHSERROR
+                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+                return {"FINISHED"}
+            if (
+                ((hloc_po - centre_0[a1]) ** 2 +
+                (vloc_po - centre_0[a2]) ** 2 -
+                radius_1 ** 2) > 0
+                ):
+                hloc_to3, hloc_to4, vloc_to3, vloc_to4 = get_tangent_points(context,
+                    centre_1[a1], centre_1[a2], radius_1, hloc_po, vloc_po
+                )
+            else:
+                pg.error = PDT_ERR_MATHSERROR
+                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+                return {"FINISHED"}
+
+        dloc_p = centre_0[a3]
+        coords_in = ((hloc_to1, vloc_to1, hloc_to2, vloc_to2, hloc_to3, vloc_to3,
+                    hloc_to4, vloc_to4, dloc_p))
+        tangent_vectors = make_vectors(coords_in, a1, a2, a3, pg)
+        draw_tangents(tangent_vectors, obj_data)
+
+    if mode == "inner":
+        # Inner Tangents
+        hloc_pi, vloc_pi = get_tangent_intersect_inner(
+            centre_0[a1], centre_0[a2], centre_1[a1], centre_1[a2], radius_0, radius_1
+        )
+        if (
+            ((hloc_pi - centre_0[a1]) ** 2 +
+            (vloc_pi - centre_0[a2]) ** 2 -
+            radius_0 ** 2) > 0
+            ):
+            hloc_to1, hloc_to2, vloc_to1, vloc_to2 = get_tangent_points(context,
+                centre_0[a1], centre_0[a2], radius_0, hloc_pi, vloc_pi
+            )
+        else:
+            pg.error = PDT_ERR_MATHSERROR
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+            return {"FINISHED"}
+        if (
+            ((hloc_pi - centre_0[a1]) ** 2 +
+            (vloc_pi - centre_0[a2]) ** 2 -
+            radius_0 ** 2) > 0
+            ):
+            hloc_to3, hloc_to4, vloc_to3, vloc_to4 = get_tangent_points(context,
+                centre_1[a1], centre_1[a2], radius_1, hloc_pi, vloc_pi
+            )
+        else:
+            pg.error = PDT_ERR_MATHSERROR
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+            return {"FINISHED"}
+
+        dloc_p = centre_0[a3]
+        coords_in = ((hloc_to1, vloc_to1, hloc_to2, vloc_to2, hloc_to3, vloc_to3,
+                    hloc_to4, vloc_to4, dloc_p))
+        tangent_vectors = make_vectors(coords_in, a1, a2, a3, pg)
+        draw_tangents(tangent_vectors, obj_data)
+
+
+def draw_tangents(tangent_vectors, obj_data):
+    """Add Edges Representing the Tangents.
+
+    Note:
+        The length of the tanget_vectors determins whcih tangents will be
+        drawn, 3 gives Point Tangents, 4 gives Inner/Outer tangents
+
+    Args:
+        tangent_vectors: A list of vectores representing the tangents
+        obj_data: A list giving Object, Object Location and Object Bmesh
+
+    Returns:
+        Nothing.
+    """
+    obj = obj_data[0]
+    obj_loc = obj_data[1]
+    bm = obj_data[2]
+    if len(tangent_vectors) == 3:
+        point_vertex_outer = bm.verts.new(tangent_vectors[0] - obj_loc)
+        tangent_vertex_o1 = bm.verts.new(tangent_vectors[1] - obj_loc)
+        tangent_vertex_o2 = bm.verts.new(tangent_vectors[2] - obj_loc)
+        bm.edges.new([tangent_vertex_o1, point_vertex_outer])
+        bm.edges.new([tangent_vertex_o2, point_vertex_outer])
+    else:
+        tangent_vertex_o1 = bm.verts.new(tangent_vectors[0] - obj_loc)
+        tangent_vertex_o2 = bm.verts.new(tangent_vectors[2] - obj_loc)
+        tangent_vertex_o3 = bm.verts.new(tangent_vectors[1] - obj_loc)
+        tangent_vertex_o4 = bm.verts.new(tangent_vectors[3] - obj_loc)
+        bm.edges.new([tangent_vertex_o1, tangent_vertex_o2])
+        bm.edges.new([tangent_vertex_o3, tangent_vertex_o4])
     bmesh.update_edit_mesh(obj.data)
 
 
 def analyse_arc(context, pg):
+    """Analyses an Arc inferred from Selected Vertices.
+
+    Note:
+        Will work if more than 3 vertices are selected, taking the
+        first, the nearest to the middle and the last.
+
+    Args:
+        context: Blender bpy.context instance
+        pg: PDT Parameters Group - our variables
+
+    Returns:
+        vector_delta: Location of Arc Centre
+        radius: Radius of Arc.
+    """
     obj = context.view_layer.objects.active
     if obj is None:
         pg.error = PDT_ERR_NO_ACT_OBJ
@@ -122,17 +397,17 @@ def analyse_arc(context, pg):
         obj_loc = obj.matrix_world.decompose()[0]
         bm = bmesh.from_edit_mesh(obj.data)
         verts = [v for v in bm.verts if v.select]
-        if len(verts) != 3:
+        if len(verts) < 3:
             pg.error = f"{PDT_ERR_SEL_3_VERTS} {len(verts)})"
             context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
             raise PDT_SelectionError
         vector_a = verts[0].co
-        vector_b = verts[1].co
-        vector_c = verts[2].co
+        vector_b = verts[int(floor(len(verts)/2))].co
+        vector_c = verts[-1].co
         vector_delta, radius = arc_centre(vector_a, vector_b, vector_c)
 
         return vector_delta, radius
-        
+
 
 class PDT_OT_TangentOperate(Operator):
     """Calculate Tangents."""
@@ -150,108 +425,146 @@ class PDT_OT_TangentOperate(Operator):
         return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
 
     def execute(self, context):
+        """Repeat Current Command Line Input.
+
+        Note:
+            Uses pg.plane, pg.tangent_point0, pg.tangent_radius0, pg.tangent_point1
+            pg.tangent_radius1, pg.tangent_point2 to place tangents.
+
+            Analyses distance between arc centres, or arc centre and tangent point
+            to determine which mode is possible (Inner, Outer, or Point). If centres are
+            both contianed within 1 inferred circle, Inner tangents are not possible.
+
+            Arcs of same radius will have no intersection for outer tangents so these
+            are calculated differently.
+
+        Args:
+            context: Blender bpy.context instance.
+
+        Returns:
+            Nothing.
+        """
+
         scene = context.scene
         pg = scene.pdt_pg
-        centre_0 = pg.tangent_point0
+        plane = pg.plane
+        # Get Object
+        obj = context.view_layer.objects.active
+        if obj is not None:
+            if obj.mode not in {"EDIT"} or obj.type != "MESH":
+                pg.error = PDT_OBJ_MODE_ERROR
+                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+                return {"FINISHED"}
+        else:
+            pg.error = PDT_ERR_NO_ACT_OBJ
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+            return {"FINISHED"}
+        bm = bmesh.from_edit_mesh(obj.data)
+        obj_loc = obj.matrix_world.decompose()[0]
+        obj_data = ((obj, obj_loc, bm))
+
         radius_0 = pg.tangent_radius0
-        centre_1 = pg.tangent_point1
         radius_1 = pg.tangent_radius1
+        centre_0 = pg.tangent_point0
+        centre_1 = pg.tangent_point1
         centre_2 = pg.tangent_point2
-        distance = (centre_0 - centre_1).length
-        if distance > radius_0 + radius_1:
-            mode = "inner"
-        elif distance > radius_0 and distance > radius_1:
-            mode = "outer"
-        else:
-            # Cannot execute, centres are too close.
-            print("Execution Error")
-            return {"FINISHED"}
 
+        tangent_setup(context, pg, plane, obj_data, centre_0, centre_1,
+            centre_2, radius_0, radius_1)
+
+        return {"FINISHED"}
+
+
+class PDT_OT_TangentOperateSel(Operator):
+    """Calculate Tangents."""
+
+    bl_idname = "pdt.tangentoperatesel"
+    bl_label = "Calculate Tangents"
+    bl_options = {"REGISTER", "UNDO"}
+    bl_description = "Calculate Tangents to Arcs from 2 Selected Vertices, or 1 & Point"
+
+    @classmethod
+    def poll(cls, context):
+        ob = context.object
+        if ob is None:
+            return False
+        return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
+
+    def execute(self, context):
+        """Repeat Current Command Line Input.
+
+        Note:
+            Uses pg.plane & 2 or more selected Vertices to place tangents.
+            One vertex must be on each arc.
+
+            Analyses distance between arc centres, or arc centre and tangent point
+            to determine which mode is possible (Inner, Outer, or Point). If centres are
+            both contianed within 1 inferred circle, Inner tangents are not possible.
+
+            Arcs of same radius will have no intersection for outer tangents so these
+            are calculated differently.
+
+        Args:
+            context: Blender bpy.context instance.
+
+        Returns:
+            Nothing.
+        """
+
+        scene = context.scene
+        pg = scene.pdt_pg
+        plane = pg.plane
         # Get Object
         obj = context.view_layer.objects.active
         if obj is not None:
             if obj.mode not in {"EDIT"} or obj.type != "MESH":
                 pg.error = PDT_OBJ_MODE_ERROR
                 context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
-                raise PDT_ObjectModeError
+                return {"FINISHED"}
         else:
             pg.error = PDT_ERR_NO_ACT_OBJ
             context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
-            raise PDT_NoObjectError
+            return {"FINISHED"}
         bm = bmesh.from_edit_mesh(obj.data)
         obj_loc = obj.matrix_world.decompose()[0]
+        obj_data = ((obj, obj_loc, bm))
 
-        if pg.tangent_from_point:
-            # FIXME Get Proper Axes Values
-            xloc_to1, xloc_to2, yloc_to1, yloc_to2 = get_tangent_points(
-                centre_0.x, centre_0.z, radius_0, centre_2.x, centre_2.z
-            )
-            # Point Tangents
-            point_vector_outer = Vector((centre_2.x, 0, centre_2.z))
-            point_vertex_outer = bm.verts.new(point_vector_outer - obj_loc)
-            tangent_vector_o1 = Vector((xloc_to1, 0, yloc_to1))
-            tangent_vertex_o1 = bm.verts.new(tangent_vector_o1 - obj_loc)
-            tangent_vector_o2 = Vector((xloc_to2, 0, yloc_to2))
-            tangent_vertex_o2 = bm.verts.new(tangent_vector_o2 - obj_loc)
-            bm.edges.new([tangent_vertex_o1, point_vertex_outer])
-            bm.edges.new([tangent_vertex_o2, point_vertex_outer])
-            bmesh.update_edit_mesh(obj.data)
+        # Get All Values from Selected Vertices
+        verts = [v for v in bm.verts if v.select]
+        v1 = verts[0]
+        vn = verts[-1]
+        for v in bm.verts:
+            v.select_set(False)
+        for e in bm.edges:
+            e.select_set(False)
+        v1.select_set(True)
+        bpy.ops.mesh.select_linked()
+        verts1 = [v for v in bm.verts if v.select].copy()
+        if len(verts1) < 3:
+            pg.error = f"{PDT_ERR_VERT_MODE}"
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
             return {"FINISHED"}
+        for v in bm.verts:
+            v.select_set(False)
+        for e in bm.edges:
+            e.select_set(False)
+        vn.select_set(True)
+        bpy.ops.mesh.select_linked()
+        vertsn = [v for v in bm.verts if v.select].copy()
+        for v in bm.verts:
+            v.select_set(False)
+        for e in bm.edges:
+            e.select_set(False)
+        bmesh.update_edit_mesh(obj.data)
+        bm.select_history.clear()
+        verts1 = [verts1[0].co, verts1[int(floor(len(verts1)/2))].co, verts1[-1].co]
+        vertsn = [vertsn[0].co, vertsn[int(floor(len(vertsn)/2))].co, vertsn[-1].co]
+        centre_0, radius_0 = arc_centre(verts1[0], verts1[1], verts1[2])
+        centre_1, radius_1 = arc_centre(vertsn[0], vertsn[1], vertsn[2])
+        centre_2 = pg.tangent_point2
 
-        if mode in {"outer", "inner"}:
-            # FIXME Get Proper Axes Values
-            xloc_po, yloc_po = get_tangent_intersect_outer(
-                centre_0.x, centre_0.z, centre_1.x, centre_1.z, radius_0, radius_1
-            )
-            # Outer Tangents
-            xloc_to1, xloc_to2, yloc_to1, yloc_to2 = get_tangent_points(
-                centre_0.x, centre_0.z, radius_0, xloc_po, yloc_po
-            )
-            xloc_to3, xloc_to4, yloc_to3, yloc_to4 = get_tangent_points(
-                centre_1.x, centre_1.z, radius_1, xloc_po, yloc_po
-            )
-
-            # Add Outer Tangent Vertices
-            draw_tangents(
-                xloc_to1,
-                xloc_to2,
-                yloc_to1,
-                yloc_to2,
-                xloc_to3,
-                xloc_to4,
-                yloc_to3,
-                yloc_to4,
-                bm,
-                obj,
-                obj_loc,
-            )
-
-        if mode == "inner":
-            # FIXME Get Proper Axes Values
-            xloc_pi, yloc_pi = get_tangent_intersect_inner(
-                centre_0.x, centre_0.z, centre_1.x, centre_1.z, radius_0, radius_1
-            )
-            # Inner Tangents
-            xloc_to1, xloc_to2, yloc_to1, yloc_to2 = get_tangent_points(
-                centre_0.x, centre_0.z, radius_0, xloc_pi, yloc_pi
-            )
-            xloc_to3, xloc_to4, yloc_to3, yloc_to4 = get_tangent_points(
-                centre_1.x, centre_1.z, radius_1, xloc_pi, yloc_pi
-            )
-            # Add Inner Tangent Vertices
-            draw_tangents(
-                xloc_to1,
-                xloc_to2,
-                yloc_to1,
-                yloc_to2,
-                xloc_to3,
-                xloc_to4,
-                yloc_to3,
-                yloc_to4,
-                bm,
-                obj,
-                obj_loc,
-            )
+        tangent_setup(context, pg, plane, obj_data, centre_0, centre_1, centre_2,
+            radius_0, radius_1)
 
         return {"FINISHED"}
 
@@ -290,10 +603,10 @@ class PDT_OT_TangentSet2(Operator):
 
     @classmethod
     def poll(cls, context):
-        ob = context.object
-        if ob is None:
+        obj = context.object
+        if obj is None:
             return False
-        return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
+        return all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"])
 
     def execute(self, context):
         scene = context.scene
@@ -302,3 +615,80 @@ class PDT_OT_TangentSet2(Operator):
         pg.tangent_point1 = vector_delta
         pg.tangent_radius1 = radius
         return {"FINISHED"}
+
+
+class PDT_OT_TangentSet3(Operator):
+    """Set Tangent Origin Point from Cursor."""
+
+    bl_idname = "pdt.tangentset3"
+    bl_label = "Set Tangent Origin Point from Cursor"
+    bl_options = {"REGISTER", "UNDO"}
+    bl_description = "Set Tangent Origin Point from Cursor"
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.object
+        if obj is None:
+            return False
+        return all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"])
+
+    def execute(self, context):
+        scene = context.scene
+        pg = scene.pdt_pg
+        pg.tangent_point2 = scene.cursor.location
+        return {"FINISHED"}
+
+
+class PDT_OT_TangentSet4(Operator):
+    """Set Tangent Origin Point from Cursor."""
+
+    bl_idname = "pdt.tangentset4"
+    bl_label = "Set Tangent Origin Point from Vertex"
+    bl_options = {"REGISTER", "UNDO"}
+    bl_description = "Set Tangent Origin Point from Vertex"
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.object
+        if obj is None:
+            return False
+        return all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"])
+
+    def execute(self, context):
+        scene = context.scene
+        pg = scene.pdt_pg
+        obj = context.object
+        bm = bmesh.from_edit_mesh(obj.data)
+        verts = [v for v in bm.verts if v.select]
+        if len(verts) != 1:
+            pg.error = f"{PDT_ERR_SEL_1_VERT} {len(verts)})"
+            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
+            raise PDT_SelectionError
+        pg.tangent_point2 = verts[0].co
+        return {"FINISHED"}
+
+
+class PDT_OT_TangentExpandMenu(Operator):
+    """Expand/Collapse Tangent Menu."""
+
+    bl_idname = "pdt.tangentexpandmenu"
+    bl_label = "Expand/Collapse Tangent Menu"
+    bl_options = {"REGISTER", "UNDO"}
+    bl_description = "Expand/Collapse Tangent Menu to Show/Hide Input Options"
+
+    def execute(self,context):
+        """Expand Menu.
+
+        Args:
+            context: Blender bpy.context instance.
+
+        Returns:
+            Nothing.
+        """
+        scene = context.scene
+        pg = scene.pdt_pg
+        if pg.menu_expand:
+            pg.menu_expand = False
+        else:
+            pg.menu_expand = True
+        return {"FINISHED"}
diff --git a/precision_drawing_tools/__init__.py b/precision_drawing_tools/__init__.py
index 298b2d9ce..4aeecd260 100644
--- a/precision_drawing_tools/__init__.py
+++ b/precision_drawing_tools/__init__.py
@@ -413,6 +413,9 @@ class PDTSceneProperties(PropertyGroup):
         name="Coordst3", default=(0.0, 0.0, 0.0), subtype="XYZ", description=PDT_DES_TANCEN3
     )
     tangent_from_point: BoolProperty(name="From Point", default=False, description=PDT_DES_TPOINT)
+    menu_expand: BoolProperty(
+        name="Expand", default=False, description="Expand/Collapse Menu",
+    )
 
 
 class PDTPreferences(AddonPreferences):
@@ -502,8 +505,12 @@ classes = (
     pdt_pivot_point.PDT_OT_PivotWrite,
     pdt_pivot_point.PDT_OT_PivotRead,
     pdt_tangent.PDT_OT_TangentOperate,
+    pdt_tangent.PDT_OT_TangentOperateSel,
     pdt_tangent.PDT_OT_TangentSet1,
     pdt_tangent.PDT_OT_TangentSet2,
+    pdt_tangent.PDT_OT_TangentSet3,
+    pdt_tangent.PDT_OT_TangentSet4,
+    pdt_tangent.PDT_OT_TangentExpandMenu,
     pdt_view.PDT_OT_ViewRot,
     pdt_view.PDT_OT_ViewRotL,
     pdt_view.PDT_OT_ViewRotR,
diff --git a/precision_drawing_tools/pdt_exception.py b/precision_drawing_tools/pdt_exception.py
index d0dc157a9..601f51413 100644
--- a/precision_drawing_tools/pdt_exception.py
+++ b/precision_drawing_tools/pdt_exception.py
@@ -85,3 +85,7 @@ class ShaderError(Exception):
 class FeatureError(Exception):
     """Wrong Feature Type Error Exception."""
     pass
+
+class DistanceError(Exception):
+    """Invalid Distance (Separation) Error."""
+    pass
diff --git a/precision_drawing_tools/pdt_functions.py b/precision_drawing_tools/pdt_functions.py
index 3d12c4f99..38879618f 100644
--- a/precision_drawing_tools/pdt_functions.py
+++ b/precision_drawing_tools/pdt_functions.py
@@ -111,6 +111,7 @@ def set_mode(mode_pl):
 
     Note:
         Sets indices of axes for locational vectors:
+        a3 is normal to screen, or depth
         "XY": a1 = x, a2 = y, a3 = z
         "XZ": a1 = x, a2 = z, a3 = y
         "YZ": a1 = y, a2 = z, a3 = x
@@ -126,6 +127,7 @@ def set_mode(mode_pl):
         "XY": (0, 1, 2),
         "XZ": (0, 2, 1),
         "YZ": (1, 2, 0),
+        "LO": (0, 1, 2),
     }
     return order[mode_pl]
 
diff --git a/precision_drawing_tools/pdt_menus.py b/precision_drawing_tools/pdt_menus.py
index 1632730c1..649b2b962 100644
--- a/precision_drawing_tools/pdt_menus.py
+++ b/precision_drawing_tools/pdt_menus.py
@@ -426,30 +426,50 @@ class PDT_PT_PanelTangent(Panel):
     def draw(self,context):
         layout = self.layout
         pdt_pg = context.scene.pdt_pg
-        # First Centre & Radius
-        row = layout.row()
-        split = row.split(factor=0.35, align=True)
-        split.label(text="Centre 1")
-        split.prop(pdt_pg, "tangent_point0", text="")
-        row = layout.row()
-        split = row.split(factor=0.45, align=False)
-        split.operator("pdt.tangentset1", text="Set From Arc")
-        split.prop(pdt_pg, "tangent_radius0", text="")
 
-        # Second Centre & Radius
+        if pdt_pg.menu_expand:
+            icon_e = "EVENT_C"
+        else:
+            icon_e = "EVENT_E"
         row = layout.row()
-        split = row.split(factor=0.35, align=True)
-        split.label(text="Centre 2")
-        split.prop(pdt_pg, "tangent_point1", text="")
+        row.label(text=f"Working {PDT_LAB_PLANE}:")
+        row.prop(pdt_pg, "plane", text="")
         row = layout.row()
-        split = row.split(factor=0.45, align=False)
-        split.operator("pdt.tangentset2", text="Set From Arc")
-        split.prop(pdt_pg, "tangent_radius1", text="")
-
+        row.operator("pdt.tangentoperatesel", text="Tangents from Selection", icon="NONE")
         row = layout.row()
-        row.operator("pdt.tangentoperate", text="Execute", icon="NONE")
+        row.label(text="Or Use Tangents From Inputs")
+        row.operator("pdt.tangentexpandmenu", text="", icon=icon_e)
+
+        box = layout.box()
+        row = box.row()
         row.prop(pdt_pg, "tangent_from_point", text="From Point")
-        row = layout.row()
+        row = box.row()
         split = row.split(factor=0.35, align=True)
         split.label(text="Tan Point")
         split.prop(pdt_pg, "tangent_point2", text="")
+        row = box.row()
+        row.operator("pdt.tangentset3", text="from Cursor")
+        row.operator("pdt.tangentset4", text="from Vertex")
+
+        if pdt_pg.menu_expand:
+            box = layout.box()
+            row = box.row()
+            split = row.split(factor=0.35, align=True)
+            split.label(text="Centre 1")
+            split.prop(pdt_pg, "tangent_point0", text="")
+            row = box.row()
+            split = row.split(factor=0.45, align=False)
+            split.operator("pdt.tangentset1", text="Set From Arc")
+            split.prop(pdt_pg, "tangent_radius0", text="")
+
+            # Second Centre & Radius
+            row = box.row()
+            split = row.split(factor=0.35, align=True)
+            split.label(text="Centre 2")
+            split.prop(pdt_pg, "tangent_point1", text="")
+            row = box.row()
+            split = row.split(factor=0.45, align=False)
+            split.operator("pdt.tangentset2", text="Set From Arc")
+            split.prop(pdt_pg, "tangent_radius1", text="")
+            row = box.row()
+            row.operator("pdt.tangentoperate", text="Tangents From Inputs", icon="NONE")
diff --git a/precision_drawing_tools/pdt_msg_strings.py b/precision_drawing_tools/pdt_msg_strings.py
index 9eeb08f17..0233a8c1f 100644
--- a/precision_drawing_tools/pdt_msg_strings.py
+++ b/precision_drawing_tools/pdt_msg_strings.py
@@ -151,6 +151,9 @@ PDT_ERR_2CPNPE = "Select 2 Co-Planar Non-Parallel Edges"
 PDT_ERR_NCEDGES = "Edges must be Co-Planar Non-Parallel Edges, Selected Edges aren't"
 PDT_ERR_1EDGE1FACE = "Select 1 face and 1 Detached Edge"
 PDT_ERR_NOINT = "No Intersection Found"
+PDT_ERR_BADDISTANCE = "Invalid Distance (Separtion) Error; Chosen Points too Close"
+PDT_ERR_MATHSERROR = "Maths Error - Check Working Plane"
+PDT_ERR_SAMERADII = "Circles have the same radius - Just offset the Edge between centres"
 
 # Info messages
 #
-- 
GitLab