Skip to content
Snippets Groups Projects
pdt_command.py 37.8 KiB
Newer Older
# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
#
# -----------------------------------------------------------------------
# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019
# -----------------------------------------------------------------------
#
import bpy
import bmesh
Alan Odom's avatar
Alan Odom committed
import math
from bpy.types import Operator
from mathutils import Vector
from .pdt_functions import (
    debug,
    intersection,
    obj_check,
    update_sel,
    view_coords,
    view_dir,
from .pdt_command_functions import (
    vector_build,
    join_two_vertices,
    set_angle_distance_two,
    set_angle_distance_three,
    origin_to_cursor,
    taper,
    placement_normal,
    placement_arc_centre,
    placement_intersect,
)
from .pdt_msg_strings import (
    PDT_ERR_ADDVEDIT,
    PDT_ERR_BADFLETTER,
    PDT_ERR_CHARS_NUM,
    PDT_ERR_DUPEDIT,
    PDT_ERR_EXTEDIT,
    PDT_ERR_FACE_SEL,
    PDT_ERR_FILEDIT,
    PDT_ERR_NON_VALID,
    PDT_ERR_NO_SEL_GEOM,
    PDT_ERR_SEL_1_EDGE,
    PDT_ERR_SEL_1_EDGEM,
Alan Odom's avatar
Alan Odom committed
    PDT_ERR_SPLITEDIT,
    PDT_ERR_BADMATHS,
    PDT_OBJ_MODE_ERROR,
    PDT_ERR_SEL_4_VERTS,
    PDT_ERR_INT_LINES,
    PDT_LAB_PLANE,
    PDT_ERR_NO_ACT_OBJ,
    PDT_ERR_VERT_MODE,
from .pdt_bix import add_line_to_bisection
from .pdt_etof import extend_vertex
from .pdt_xall import intersect_all
Alan Odom's avatar
Alan Odom committed
from . import pdt_exception
PDT_SelectionError = pdt_exception.SelectionError
PDT_InvalidVector = pdt_exception.InvalidVector
PDT_CommandFailure = pdt_exception.CommandFailure
PDT_ObjectModeError = pdt_exception.ObjectModeError
PDT_MathsError = pdt_exception.MathsError
PDT_IntersectionError = pdt_exception.IntersectionError
PDT_NoObjectError = pdt_exception.NoObjectError
PDT_FeatureError = pdt_exception.FeatureError
class PDT_OT_CommandReRun(Operator):
    """Repeat Current Displayed Command."""

    bl_idname = "pdt.command_rerun"
    bl_label = "Re-run Current Command"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        """Repeat Current Command Line Input.

        Args:
            context: Blender bpy.context instance.

        Returns:
            Nothing.
        """
        command_run(self, context)
        return {"FINISHED"}


def command_run(self, context):
    """Run Command String as input into Command Line.

    Note:
        Uses pg.command, pg.error & many other 'pg.' variables to set PDT menu items,
        or alter functions

        Command Format; Operation(single letter) Mode(single letter) Values(up to 3 values
        separated by commas)

        Example; CD0.4,0.6,1.1 - Moves Cursor Delta XYZ = 0.4,0.6,1.1 from Current Position/Active
        Vertex/Object Origin

        Example; SP35 - Splits active Edge at 35% of separation between edge's vertices

        Valid First Letters (as 'operation' - pg.command[0])
            C = Cursor, G = Grab(move), N = New Vertex, V = Extrude Vertices Only,
            E = Extrude geometry, P = Move Pivot Point, D = Duplicate geometry, S = Split Edges

            Capitals and lower case letters are both allowed

        Valid Second Letters (as 'mode' - pg.command[1])

            A = Absolute XYZ, D = Delta XYZ, I = Distance at Angle, P = Percent
            X = X Delta, Y = Y, Delta Z, = Z Delta, O = Output (Maths Operation only)
            V = Vertex Bevel, E = Edge Bevel, I = Intersect then Bevel

            Capitals and lower case letters are both allowed

        Valid Values (pdt_command[2:])
            Only Integers and Floats, missing values are set to 0, appropriate length checks are
            performed as Values is split by commas.

            Example; CA,,3 - Cursor to Absolute, is re-interpreted as CA0,0,3

            Exception for Maths Operation, Values section is evaluated as Maths expression

            Example; madegrees(atan(3/4)) - sets PDT Angle to smallest angle of 3,4,5 Triangle;
            (36.8699 degrees)

    Args:
        context: Blender bpy.context instance.

    Returns:
        Nothing.
    """

    scene = context.scene
    pg = scene.pdt_pg
    command = pg.command.strip()
Alan Odom's avatar
Alan Odom committed
    # Check Object Type & Mode First
    obj = context.view_layer.objects.active
    if obj is not None and command[0].upper() not in {"M", "?", "HELP"}:
        if obj.mode not in {"OBJECT", "EDIT"} or obj.type not in {"MESH", "EMPTY"}:
Alan Odom's avatar
Alan Odom committed
            pg.error = PDT_OBJ_MODE_ERROR
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            raise PDT_ObjectModeError
Alan Odom's avatar
Alan Odom committed

    # Special Cases of Command.
    if command == "?" or command.lower() == "help":
        # fmt: off
        context.window_manager.popup_menu(pdt_help, title="PDT Command Line Help", icon="INFO")
        # fmt: on
        return
    if command.upper() == "J2V":
        join_two_vertices(context)
    if command.upper() == "AD2":
        set_angle_distance_two(context)
    if command.upper() == "AD3":
        set_angle_distance_three(context)
    if command.upper() == "OTC":
        origin_to_cursor(context)
    if command.upper() == "TAP":
Alan Odom's avatar
Alan Odom committed
        taper(context)
    if command.upper() == "BIS":
        add_line_to_bisection(context)
    if command.upper() == "ETF":
Alan Odom's avatar
Alan Odom committed
        extend_vertex(context)
    if command.upper() == "INTALL":
        intersect_all(context)
    if command.upper()[1:] == "NML":
        placement_normal(context, command.upper()[0])
    if command.upper()[1:] == "CEN":
        placement_arc_centre(context, command.upper()[0])
    if command.upper()[1:] == "INT":
        placement_intersect(context, command.upper()[0])
Alan Odom's avatar
Alan Odom committed

    # Check Command Length
Alan Odom's avatar
Alan Odom committed
    if len(command) < 3:
        pg.error = PDT_ERR_CHARS_NUM
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
Alan Odom's avatar
Alan Odom committed

    # Check First Letter
    operation = command[0].upper()
    if operation not in {"C", "D", "E", "F", "G", "N", "M", "P", "V", "S"}:
        pg.error = PDT_ERR_BADFLETTER
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    # Check Second Letter.
Alan Odom's avatar
Alan Odom committed
    mode = command[1].lower()
    if (
            (operation == "F" and mode not in {"v", "e", "i"})
            or (operation in {"D", "E"} and mode not in {"d", "i"})
            or (operation == "M" and mode not in {"a", "d", "i", "p", "o", "x", "y", "z"})
            or (operation not in {"D", "E", "F", "M"} and mode not in {"a", "d", "i", "p"})
        ):
Alan Odom's avatar
Alan Odom committed
        pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return

    # --------------
    # Maths Operation
    if operation == "M":
Alan Odom's avatar
Alan Odom committed
        try:
            command_maths(context, mode, pg, command[2:], mode)
Alan Odom's avatar
Alan Odom committed
            return
Alan Odom's avatar
Alan Odom committed
        except PDT_MathsError:
            return

    # -----------------------------------------------------
    # Not a Maths Operation, so let's parse the command line
Alan Odom's avatar
Alan Odom committed
    try:
        pg, values, obj, obj_loc, bm, verts = command_parse(context)
    except PDT_SelectionError:
        return

    # ---------------------
    # Cursor or Pivot Point
    if operation in {"C", "P"}:
        try:
            move_cursor_pivot(context, pg, operation, mode, obj, verts, values)
        except PDT_CommandFailure:
            return

    # ------------------------
    # Move Vertices or Objects
    if operation == "G":
Alan Odom's avatar
Alan Odom committed
        try:
            move_entities(context, pg, operation, mode, obj, bm, verts, values)
        except PDT_CommandFailure:
            return

    # --------------
    # Add New Vertex
    if operation == "N":
Alan Odom's avatar
Alan Odom committed
        try:
            add_new_vertex(context, pg, operation, mode, obj, bm, verts, values)
        except PDT_CommandFailure:
            return

    # -----------
    # Split Edges
    if operation == "S":
Alan Odom's avatar
Alan Odom committed
        try:
            split_edges(context, pg, operation, mode, obj, obj_loc, bm, values)
        except PDT_CommandFailure:
            return


    # ----------------
    # Extrude Vertices
    if operation == "V":
Alan Odom's avatar
Alan Odom committed
        try:
            extrude_vertices(context, pg, operation, mode, obj, obj_loc, bm, verts, values)
        except PDT_CommandFailure:
            return

    # ----------------
    # Extrude Geometry
    if operation == "E":
Alan Odom's avatar
Alan Odom committed
        try:
            extrude_geometry(context, pg, operation, mode, obj, bm, values)
        except PDT_CommandFailure:
            return

    # ------------------
    # Duplicate Geometry
    if operation == "D":
Alan Odom's avatar
Alan Odom committed
        try:
            duplicate_geometry(context, pg, operation, mode, obj, bm, values)
        except PDT_CommandFailure:
            return

    # ---------------
    # Fillet Geometry
    if operation == "F":
Alan Odom's avatar
Alan Odom committed
        try:
            fillet_geometry(context, pg, mode, obj, bm, verts, values)
        except PDT_CommandFailure:
            return


def pdt_help(self, context):
    """Display PDT Command Line help in a pop-up.

    Args:
        context: Blender bpy.context instance

    Returns:
        Nothing.
    """
Alan Odom's avatar
Alan Odom committed
    label = self.layout.label
    label(text="Primary Letters (Available Secondary Letters):")
    label(text="")
    label(text="C: Cursor (a, d, i, p)")
    label(text="D: Duplicate Geometry (d, i)")
    label(text="E: Extrude Geometry (d, i)")
    label(text="F: Fillet (v, e, i)")
Alan Odom's avatar
Alan Odom committed
    label(text="G: Grab (Move) (a, d, i, p)")
    label(text="N: New Vertex (a, d, i, p)")
    label(text="M: Maths Functions (a, d, p, o, x, y, z)")
Alan Odom's avatar
Alan Odom committed
    label(text="P: Pivot Point (a, d, i, p)")
    label(text="V: Extrude Vertice Only (a, d, i, p)")
    label(text="S: Split Edges (a, d, i, p)")
    label(text="?: Quick Help")
    label(text="")
    label(text="Secondary Letters:")
    label(text="")
    label(text="- General Options:")
    label(text="a: Absolute (Global) Coordinates e.g. 1,3,2")
    label(text="d: Delta (Relative) Coordinates, e.g. 0.5,0,1.2")
    label(text="i: Directional (Polar) Coordinates e.g. 2.6,45")
    label(text="p: Percent e.g. 67.5")
    label(text="- Fillet Options:")
    label(text="v: Fillet Vertices")
    label(text="e: Fillet Edges")
    label(text="i: Fillet & Intersect 2 Disconnected Edges")
    label(text="- Math Options:")
    label(text="x, y, z: Send result to X, Y and Z input fields in PDT Design")
    label(text="d, a, p: Send result to Distance, Angle or Percent input field in PDT Design")
    label(text="o: Send Maths Calculation to Output")
    label(text="")
    label(text="Note that commands are case-insensitive: ED = Ed = eD = ed")
    label(text="")
    label(text="Examples:")
    label(text="")
    label(text="ed0.5,,0.6")
    label(text="'- Extrude Geometry Delta 0.5 in X, 0 in Y, 0.6 in Z")
    label(text="")
    label(text="fe0.1,4,0.5")
    label(text="'- Fillet Edges")
    label(text="'- Radius: 0.1 (float) -- the radius (or offset) of the bevel/fillet")
    label(text="'- Segments: 4 (int) -- choosing an even amount of segments gives better geometry")
    label(text="'- Profile: 0.5 (float[0.0;1.0]) -- 0.5 (default) yields a circular, convex shape")
    label(text="")
    label(text="More Information at:")
    label(text="https://github.com/Clockmender/Precision-Drawing-Tools/wiki")
Alan Odom's avatar
Alan Odom committed


def command_maths(context, mode, pg, expression, output_target):
    """Evaluates Maths Input.

    Args:
        context: Blender bpy.context instance.
        mode: The Operation Mode, e.g. a for Absolute
        pg: PDT Parameters Group - our variables
        expression: The Maths component of the command input e.g. sqrt(56)
        output_target: The output variable box on the UI
Alan Odom's avatar
Alan Odom committed

    Returns:
        Nothing.
    """

    namespace = {}
    namespace.update(vars(math))
    try:
        maths_result = eval(expression, namespace, namespace)
    except:
        pg.error = PDT_ERR_BADMATHS
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        raise PDT_MathsError
    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
Alan Odom's avatar
Alan Odom committed
    if output_target == "x":
        pg.cartesian_coords.x = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    elif output_target == "y":
        pg.cartesian_coords.y = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    elif output_target == "z":
        pg.cartesian_coords.z = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    elif output_target == "d":
        pg.distance = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    elif output_target == "a":
        pg.angle = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    elif output_target == "p":
        pg.percent = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed
    else:
        pg.maths_output = round(maths_result, decimal_places)
Alan Odom's avatar
Alan Odom committed


def command_parse(context):
    """Parse Command Input.

    Args:
        context: Blender bpy.context instance.

    Returns:
        pg: PDT Parameters Group - our variables
        values_out: The Output Values as a list of numbers
        obj: The Active Object
        obj_loc: The object's location in 3D space
        bm: The object's Bmesh
        verts: The object's selected vertices, or selected history vertices.
Alan Odom's avatar
Alan Odom committed
    scene = context.scene
    pg = scene.pdt_pg
    command = pg.command.strip()
    operation = command[0].upper()
    mode = command[1].lower()
    values = command[2:].split(",")
    mode_sel = pg.select
Alan Odom's avatar
Alan Odom committed
    obj = context.view_layer.objects.active
Alan Odom's avatar
Alan Odom committed
    for v in values:
Alan Odom's avatar
Alan Odom committed
            _ = float(v)
            good = True
        except ValueError:
Alan Odom's avatar
Alan Odom committed
            values[ind] = "0.0"
Alan Odom's avatar
Alan Odom committed
    # Apply System Rounding
    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
    values_out = [str(round(float(v), decimal_places)) for v in values]
    bm = "No Bmesh"
    obj_loc = Vector((0,0,0))
    verts = []

    if mode_sel == 'REL' and operation not in {"C", "P"}:
        pg.select = 'SEL'
        mode_sel = 'SEL'

    if mode == "a" and operation not in {"C", "P"}:
        # Place new Vetex, or Extrude Vertices by Absolute Coords.
        if mode_sel == 'REL':
            pg.select = 'SEL'
        if obj is not None:
            if obj.mode == "EDIT":
                bm = bmesh.from_edit_mesh(obj.data)
                obj_loc = obj.matrix_world.decompose()[0]
                verts = []
            else:
Alan Odom's avatar
Alan Odom committed
                if operation != "G":
                    pg.error = PDT_OBJ_MODE_ERROR
                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
                    raise PDT_ObjectModeError
        else:
            pg.error = PDT_ERR_NO_ACT_OBJ
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            raise PDT_NoObjectError
    if mode_sel == 'SEL' and mode not in {"a"}:
        # All other options except Cursor or Pivot by Absolute
        # These options require no object, etc.
        bm, good = obj_check(obj, scene, operation)
        if good and obj.mode == 'EDIT':
            obj_loc = obj.matrix_world.decompose()[0]
            if len(bm.select_history) == 0 or operation == "G":
                verts = [v for v in bm.verts if v.select]
                if len(verts) == 0:
                    pg.error = PDT_ERR_NO_SEL_GEOM
                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
                    raise PDT_SelectionError
            else:
                verts = bm.select_history
Alan Odom's avatar
Alan Odom committed
    debug(f"command: {operation}{mode}{values_out}")
    debug(f"obj: {obj}, bm: {bm}, obj_loc: {obj_loc}")

Alan Odom's avatar
Alan Odom committed
    return pg, values_out, obj, obj_loc, bm, verts
Alan Odom's avatar
Alan Odom committed

def move_cursor_pivot(context, pg, operation, mode, obj, verts, values):
    """Moves Cursor & Pivot Point.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        verts: The object's selected vertices, or selected history vertices
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    # Absolute/Global Coordinates, or Delta/Relative Coordinates
    if mode in {"a", "d"}:
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
    # Direction/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector
    # Percent Options
    else:
        # Must be Percent
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 1)
        except:
            raise PDT_InvalidVector

    scene = context.scene
    mode_sel = pg.select
    obj_loc = Vector((0,0,0))
    if obj is not None:
        obj_loc = obj.matrix_world.decompose()[0]
Alan Odom's avatar
Alan Odom committed

    if mode == "a":
        if operation == "C":
            scene.cursor.location = vector_delta
        elif operation == "P":
            pg.pivot_loc = vector_delta
    elif mode in {"d", "i"}:
        if pg.plane == "LO" and mode == "d":
            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
        elif pg.plane == "LO" and mode == "i":
            vector_delta = view_dir(pg.distance, pg.angle)
Alan Odom's avatar
Alan Odom committed
        if mode_sel == "REL":
            if operation == "C":
                scene.cursor.location = scene.cursor.location + vector_delta
            else:
                pg.pivot_loc = pg.pivot_loc + vector_delta
        elif mode_sel == "SEL":
            if obj.mode == "EDIT":
Alan Odom's avatar
Alan Odom committed
                if operation == "C":
                    scene.cursor.location = verts[-1].co + obj_loc + vector_delta
                else:
                    pg.pivot_loc = verts[-1].co + obj_loc + vector_delta
            if obj.mode == "OBJECT":
                if operation == "C":
                    scene.cursor.location = obj_loc + vector_delta
                else:
                    pg.pivot_loc = obj_loc + vector_delta
    else:
        # Must be Percent
        if obj.mode == "EDIT":
            if operation == "C":
                scene.cursor.location = obj_loc + vector_delta
Alan Odom's avatar
Alan Odom committed
                pg.pivot_loc = obj_loc + vector_delta
        if obj.mode == "OBJECT":
            if operation == "C":
                scene.cursor.location = vector_delta
            else:
                pg.pivot_loc = vector_delta
Alan Odom's avatar
Alan Odom committed
def move_entities(context, pg, operation, mode, obj, bm, verts, values):
    """Moves Entities.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        bm: The object's Bmesh
        verts: The object's selected vertices, or selected history vertices
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    obj_loc = obj.matrix_world.decompose()[0]
Alan Odom's avatar
Alan Odom committed
    # Absolute/Global Coordinates
    if mode == "a":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        if obj.mode == "EDIT":
            for v in verts:
Alan Odom's avatar
Alan Odom committed
                v.co = vector_delta - obj_loc
            bmesh.ops.remove_doubles(
                bm, verts=[v for v in bm.verts if v.select], dist=0.0001
            )
Alan Odom's avatar
Alan Odom committed
        if obj.mode == "OBJECT":
            for ob in context.view_layer.objects.selected:
                ob.location = vector_delta
Alan Odom's avatar
Alan Odom committed
    elif mode in {"d", "i"}:
        if mode == "d":
            # Delta/Relative Coordinates
            try:
                vector_delta = vector_build(context, pg, obj, operation, values, 3)
            except:
                raise PDT_InvalidVector
        else:
            # Direction/Polar Coordinates
            try:
                vector_delta = vector_build(context, pg, obj, operation, values, 2)
            except:
                raise PDT_InvalidVector

        if pg.plane == "LO" and mode == "d":
            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
        elif pg.plane == "LO" and mode == "i":
            vector_delta = view_dir(pg.distance, pg.angle)

Alan Odom's avatar
Alan Odom committed
        if obj.mode == "EDIT":
            bmesh.ops.translate(
                bm, verts=[v for v in bm.verts if v.select], vec=vector_delta
            )
        if obj.mode == "OBJECT":
            for ob in context.view_layer.objects.selected:
                ob.location = obj_loc + vector_delta
    # Percent Options Only Other Choice
    else:
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 1)
        except:
            raise PDT_InvalidVector
        if obj.mode == 'EDIT':
            verts[-1].co = vector_delta
        if obj.mode == "OBJECT":
            obj.location = vector_delta
    if obj.mode == 'EDIT':
        bmesh.update_edit_mesh(obj.data)
        bm.select_history.clear()
Alan Odom's avatar
Alan Odom committed

def add_new_vertex(context, pg, operation, mode, obj, bm, verts, values):
    """Add New Vertex.

    Args:
        context: Blender bpy.context instance.
        pg, operation, mode, obj, bm, verts, values

    Returns:
        Nothing.
    """

Alan Odom's avatar
Alan Odom committed
    obj_loc = obj.matrix_world.decompose()[0]

    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_ADDVEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        raise PDT_SelectionError
    if mode not in {"a"}:
        if not isinstance(verts[0], bmesh.types.BMVert):
            pg.error = PDT_ERR_VERT_MODE
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            raise PDT_FeatureError
Alan Odom's avatar
Alan Odom committed
    # Absolute/Global Coordinates
    if mode == "a":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        new_vertex = bm.verts.new(vector_delta - obj_loc)
    # Delta/Relative Coordinates
    elif mode == "d":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        if pg.plane == "LO":
            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
Alan Odom's avatar
Alan Odom committed
        new_vertex = bm.verts.new(verts[-1].co + vector_delta)
    # Direction/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector
        if pg.plane == "LO":
            vector_delta = view_dir(pg.distance, pg.angle)
Alan Odom's avatar
Alan Odom committed
        new_vertex = bm.verts.new(verts[-1].co + vector_delta)
    # Percent Options Only Other Choice
    else:
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 1)
        except:
            raise PDT_InvalidVector
        new_vertex = bm.verts.new(vector_delta)

    for v in [v for v in bm.verts if v.select]:
        v.select_set(False)
    new_vertex.select_set(True)
    bmesh.update_edit_mesh(obj.data)
    bm.select_history.clear()


def split_edges(context, pg, operation, mode, obj, obj_loc, bm, values):
    """Split Edges.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        obj_loc: The object's location in 3D space
        bm: The object's Bmesh
        verts: The object's selected vertices, or selected history vertices
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_SPLITEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    # Absolute/Global Coordinates
    if mode == "a":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        edges = [e for e in bm.edges if e.select]
        if len(edges) != 1:
            pg.error = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
        new_vertex = new_verts[0]
        new_vertex.co = vector_delta - obj_loc
    # Delta/Relative Coordinates
    elif mode == "d":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        edges = [e for e in bm.edges if e.select]
        faces = [f for f in bm.faces if f.select]
        if len(faces) != 0:
            pg.error = PDT_ERR_FACE_SEL
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        if len(edges) < 1:
            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
        bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
    # Directional/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector
        edges = [e for e in bm.edges if e.select]
        faces = [f for f in bm.faces if f.select]
        if len(faces) != 0:
            pg.error = PDT_ERR_FACE_SEL
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        if len(edges) < 1:
            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
        bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
    # Percent Options
    elif mode == "p":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 1)
        except:
            raise PDT_InvalidVector
        edges = [e for e in bm.edges if e.select]
        faces = [f for f in bm.faces if f.select]
        if len(faces) != 0:
            pg.error = PDT_ERR_FACE_SEL
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
Alan Odom's avatar
Alan Odom committed
        if len(edges) != 1:
            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
        new_vertex = new_verts[0]
        new_vertex.co = vector_delta

    for v in [v for v in bm.verts if v.select]:
        v.select_set(False)
    for v in new_verts:
        v.select_set(False)
    bmesh.update_edit_mesh(obj.data)
    bm.select_history.clear()


def extrude_vertices(context, pg, operation, mode, obj, obj_loc, bm, verts, values):
    """Extrude Vertices.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        obj_loc: The object's location in 3D space
        bm: The object's Bmesh
        verts: The object's selected vertices, or selected history vertices
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_EXTEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    # Absolute/Global Coordinates
    if mode == "a":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        new_vertex = bm.verts.new(vector_delta - obj_loc)
        verts = [v for v in bm.verts if v.select].copy()
        for v in verts:
            bm.edges.new([v, new_vertex])
            v.select_set(False)
        new_vertex.select_set(True)
        bmesh.ops.remove_doubles(
            bm, verts=[v for v in bm.verts if v.select], dist=0.0001
Alan Odom's avatar
Alan Odom committed
    # Delta/Relative Coordinates
    elif mode == "d":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
        if pg.plane == "LO":
            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
Alan Odom's avatar
Alan Odom committed
        for v in verts:
            new_vertex = bm.verts.new(v.co)
            new_vertex.co = new_vertex.co + vector_delta
            bm.edges.new([v, new_vertex])
            v.select_set(False)
            new_vertex.select_set(True)
    # Direction/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector
        if pg.plane == "LO":
            vector_delta = view_dir(pg.distance, pg.angle)
Alan Odom's avatar
Alan Odom committed
        for v in verts:
            new_vertex = bm.verts.new(v.co)
            new_vertex.co = new_vertex.co + vector_delta
            bm.edges.new([v, new_vertex])
            v.select_set(False)
            new_vertex.select_set(True)
    # Percent Options
    elif mode == "p":
        extend_all  = pg.extend
Alan Odom's avatar
Alan Odom committed
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 1)
        except:
            raise PDT_InvalidVector
        verts = [v for v in bm.verts if v.select].copy()
        new_vertex = bm.verts.new(vector_delta)
Alan Odom's avatar
Alan Odom committed
            for v in [v for v in bm.verts if v.select]:
                bm.edges.new([v, new_vertex])
                v.select_set(False)
        else:
            bm.edges.new([verts[-1], new_vertex])
        new_vertex.select_set(True)

    bmesh.update_edit_mesh(obj.data)


def extrude_geometry(context, pg, operation, mode, obj, bm, values):
    """Extrude Geometry.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        bm: The object's Bmesh
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_EXTEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    # Delta/Relative Coordinates
    if mode == "d":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
    # Direction/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector

    ret = bmesh.ops.extrude_face_region(
        bm,
        geom=(
            [f for f in bm.faces if f.select]
            + [e for e in bm.edges if e.select]
            + [v for v in bm.verts if v.select]
        ),
        use_select_history=True,
    )
    geom_extr = ret["geom"]
    verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)]
    edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)]
    faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)]
    del ret

    if pg.plane == "LO" and mode == "d":
        vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
    elif pg.plane == "LO" and mode == "i":
        vector_delta = view_dir(pg.distance, pg.angle)

Alan Odom's avatar
Alan Odom committed
    bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta)
    update_sel(bm, verts_extr, edges_extr, faces_extr)
    bmesh.update_edit_mesh(obj.data)
    bm.select_history.clear()
Alan Odom's avatar
Alan Odom committed

def duplicate_geometry(context, pg, operation, mode, obj, bm, values):
    """Duplicate Geometry.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        bm: The object's Bmesh
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_DUPEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
Alan Odom's avatar
Alan Odom committed
    # Delta/Relative Coordinates
    if mode == "d":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 3)
        except:
            raise PDT_InvalidVector
    # Direction/Polar Coordinates
    elif mode == "i":
        try:
            vector_delta = vector_build(context, pg, obj, operation, values, 2)
        except:
            raise PDT_InvalidVector

    ret = bmesh.ops.duplicate(
        bm,
        geom=(
            [f for f in bm.faces if f.select]
            + [e for e in bm.edges if e.select]
            + [v for v in bm.verts if v.select]
        ),
        use_select_history=True,
    )
    geom_dupe = ret["geom"]
    verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)]
    edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)]
    faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)]
    del ret

    if pg.plane == "LO" and mode == "d":
        vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
    elif pg.plane == "LO" and mode == "i":
        vector_delta = view_dir(pg.distance, pg.angle)

Alan Odom's avatar
Alan Odom committed
    bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta)
    update_sel(bm, verts_dupe, edges_dupe, faces_dupe)
    bmesh.update_edit_mesh(obj.data)


def fillet_geometry(context, pg, mode, obj, bm, verts, values):
    """Fillet Geometry.

    Args:
        context: Blender bpy.context instance.
        pg: PDT Parameters Group - our variables
        operation: The Operation e.g. Create New Vertex
        mode: The Operation Mode, e.g. a for Absolute
        obj: The Active Object
        bm: The object's Bmesh
        verts: The object's selected vertices, or selected history vertices
        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
Alan Odom's avatar
Alan Odom committed
    if not obj.mode == "EDIT":
        pg.error = PDT_ERR_FILEDIT
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    if mode in {"i", "v"}:
        vert_bool = True
    else:
        # Must be "e"
Alan Odom's avatar
Alan Odom committed
        vert_bool = False
    # Note that passing an empty parameter results in that parameter being seen as "0"
    # _offset <= 0 is ignored since a bevel/fillet radius must be > 0 to make sense
    _offset = float(values[0])
Alan Odom's avatar
Alan Odom committed
    _segments = float(values[1])
Alan Odom's avatar
Alan Odom committed
    if _segments < 1:
        _segments = 1   # This is a single, flat segment (ignores profile)
    _profile = float(values[2])
    if _profile < 0.0 or _profile > 1.0:
        _profile = 0.5  # This is a circular profile
    if mode == "i":
        # Fillet & Intersect Two Edges
        # Always use Current Selection
        verts = [v for v in bm.verts if v.select]
        edges = [e for e in bm.edges if e.select]
        if len(edges) == 2 and len(verts) == 4:
            plane = pg.plane
            v_active = edges[0].verts[0]
            v_other = edges[0].verts[1]
            v_last = edges[1].verts[0]
            v_first = edges[1].verts[1]
            vector_delta, done = intersection(v_active.co,
                                              v_other.co,
                                              v_last.co,
                                              v_first.co,
                                              plane
                                              )
Alan Odom's avatar
Alan Odom committed
            if not done:
                pg.error = f"{PDT_ERR_INT_LINES} {plane}  {PDT_LAB_PLANE}"
                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
Alan Odom's avatar
Alan Odom committed
                raise PDT_IntersectionError
            if (v_active.co - vector_delta).length < (v_other.co - vector_delta).length:
                v_active.co = vector_delta
                v_other.select_set(False)
            else:
                v_other.co = vector_delta
                v_active.select_set(False)
            if (v_last.co - vector_delta).length < (v_first.co - vector_delta).length:
                v_last.co = vector_delta
                v_first.select_set(False)
            else:
                v_first.co = vector_delta
                v_last.select_set(False)
            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
        else:
            pg.error = f"{PDT_ERR_SEL_4_VERTS} {len(verts)} Vert(s), {len(edges)} Edge(s))"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            raise PDT_SelectionError

    bpy.ops.mesh.bevel(
        offset_type="OFFSET",
        offset=_offset,
        segments=_segments,
        profile=_profile,
        vertex_only=vert_bool
    )