Skip to content
Snippets Groups Projects
  • Alan Odom's avatar
    0e62a382
    PDT: Refactor - Stage 3 (Pylint & Black runs) · 0e62a382
    Alan Odom authored
    Most of the recommendations from pylint are now done, I have not changed variables
    like v in expression [v for v in... as I consider these to be normal coding practice.
    
    Rename pdt_com_functions.py to pdt_command_functions.py
    
    Fix error in Intersect operation if selected vertices resulted in more than
    2 edges being selected. Priority is given to two edges as a selection, then to
    4 vertices, if the four vertices represent two edges, the intersection point is
    at the intersection of the two edges, which might not be the four vertices, if
    one of the vertices forms part of two edges that are also selected.
    
    Priority is 2 edges, then 4 vertices selected individually with the mouse.
    0e62a382
    History
    PDT: Refactor - Stage 3 (Pylint & Black runs)
    Alan Odom authored
    Most of the recommendations from pylint are now done, I have not changed variables
    like v in expression [v for v in... as I consider these to be normal coding practice.
    
    Rename pdt_com_functions.py to pdt_command_functions.py
    
    Fix error in Intersect operation if selected vertices resulted in more than
    2 edges being selected. Priority is given to two edges as a selection, then to
    4 vertices, if the four vertices represent two edges, the intersection point is
    at the intersection of the two edges, which might not be the four vertices, if
    one of the vertices forms part of two edges that are also selected.
    
    Priority is 2 edges, then 4 vertices selected individually with the mouse.
pdt_command.py 27.06 KiB
# ***** 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
from bpy.types import Operator
from mathutils import Vector
from .pdt_functions import (
    debug,
    intersection,
    obj_check,
    oops,
    update_sel,
)
from .pdt_command_functions import (
    command_maths,
    vector_build,
    move_cursor_pivot,
    join_two_vertices,
    set_angle_distance_two,
    set_angle_distance_three,
    origin_to_cursor,
    taper,
    placement_normal,
    placement_centre,
    placement_intersect,
)
from .pdt_msg_strings import (
    PDT_ERR_ADDVEDIT,
    PDT_ERR_BAD3VALS,
    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_ACT_OBJ,
    PDT_ERR_NO_SEL_GEOM,
    PDT_ERR_SEL_1_EDGE,
    PDT_ERR_SEL_1_EDGEM,
    PDT_ERR_SEL_1_VERT,
    PDT_ERR_SPLITEDIT
)
from .pdt_bix import add_line_to_bisection
from .pdt_etof import extend_vertex
from .pdt_xall import intersect_all


def pdt_help(self, context):
    """Display PDT Command Line help in a pop-up."""
    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)")
    label(text="G: Grab (Move) (a, d, i, p)")
    label(text="N: New Vertex (a, d, i, p)")
    label(text="M: Maths Functions (x, y, z, d, a, p)")
    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")


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.

    Args:
        context: Blender bpy.context instance.

    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

            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 command

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

    Returns:
        Nothing.
    """

    scene = context.scene
    pg = scene.pdt_pg
    command = pg.command.strip()

    # Check First Letter.
    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
    elif command == "":
        return
    elif command.upper() == "J2V":
        join_two_vertices(context)
        return
    elif command.upper() == "AD2":
        set_angle_distance_two(context)
        return
    elif command.upper() == "AD3":
        set_angle_distance_three(context)
        return
    elif command.upper() == "OTC":
        origin_to_cursor(context)
        return
    elif command.upper() == "TAP":
        taper(self, context)
        return
    elif command.upper() == "BIS":
        add_line_to_bisection(context)
        return
    elif command.upper() == "ETF":
        extend_vertex(self, context)
        return
    elif command.upper() == "INTALL":
        intersect_all(context)
        return
    elif command.upper()[1:4] == "NML":
        placement_normal(context, command.upper()[0])
        return
    elif command.upper()[1:4] == "CEN":
        placement_centre(context, command.upper()[0])
        return
    elif command.upper()[1:4] == "INT":
        placement_intersect(context, command.upper()[0])
        return
    elif len(command) < 3:
        pg.error = PDT_ERR_CHARS_NUM
        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
        return
    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
    mode = command[1].lower()

    # Check Second Letter.
    if operation == "F":
        if mode not in {"v", "e", "i"}:
            pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
    elif operation in {"D", "E"}:
        if mode not in {"d", "i"}:
            pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
    elif operation == "M":
        if mode not in {"a", "d", "i", "p", "o", "x", "y", "z"}:
            pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
    else:
         if mode not in {"a", "d", "i", "p"}:
             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":
        command_maths(context, mode, pg, command[2:], mode)
        return

    # -----------------------------------------------------
    # Not a Maths Operation, so let's parse the command line
    values = command[2:].split(",")
    ind = 0
    for r in values:
        try:
            _ = float(r)
            good = True
        except ValueError:
            values[ind] = "0"
        ind = ind + 1
    mode_s = pg.select
    flip_a = pg.flip_angle
    flip_p = pg.flip_percent
    ext_a = pg.extend
    plane = pg.plane
    obj = context.view_layer.objects.active
    bm, good = obj_check(obj, scene, operation)
    if good:
        obj_loc = obj.matrix_world.decompose()[0]
    else:
        obj_loc = None

    if mode_s == 'SEL' and bm is not None and mode not in {"a"}:
        if len(bm.select_history) == 0:
            if len([v for v in bm.verts if v.select]) == 0:
                pg.error = PDT_ERR_NO_SEL_GEOM
                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
                return
            else:
                verts = [v for v in bm.verts if v.select]
        else:
            verts = bm.select_history
    else:
        verts = []

    debug(f"command: {command}")
    debug(f"obj: {obj}, bm: {bm}, obj_loc: {obj_loc}")

    # ---------------------
    # Cursor or Pivot Point
    if operation in {"C", "P"}:
        # Absolute/Global Coordinates, or Delta/Relative Coordinates
        if mode in {"a", "d"}:
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
        # Direction/Polar Coordinates
        elif mode == "i":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
        # Percent Options
        elif mode == "p":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)

        if valid_result:
            move_cursor_pivot(context, pg, obj, verts, operation,
                mode, vector_delta)
        return

    # ------------------------
    # Move Vertices or Objects
    elif operation == "G":
        if obj.mode == "EDIT":
            if len(verts) == 0:
                pg.error = PDT_ERR_NO_SEL_GEOM
                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
                return
        # Absolute/Global Coordinates
        if mode == "a":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            if obj.mode == "EDIT":
                for v in verts:
                    v.co = vector_delta - obj_loc
                bmesh.ops.remove_doubles(
                    bm, verts=[v for v in bm.verts if v.select], dist=0.0001
                )
            elif obj.mode == "OBJECT":
                for ob in context.view_layer.objects.selected:
                    ob.location = vector_delta

        elif mode in {"d", "i"}:
            if mode == "d":
                # Delta/Relative Coordinates
                valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
                if not valid_result:
                    return
            else:
                # Direction/Polar Coordinates
                valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
                if not valid_result:
                    return
            if vector_delta is not None:
                if obj.mode == "EDIT":
                    bmesh.ops.translate(
                        bm, verts=[v for v in bm.verts if v.select], vec=vector_delta
                    )
                elif obj.mode == "OBJECT":
                    for ob in context.view_layer.objects.selected:
                        ob.location = obj_loc + vector_delta
        # Percent Options
        elif mode == "p":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
            if not valid_result:
                return
            if vector_delta is not None:
                if obj.mode == 'EDIT':
                    verts[-1].co = vector_delta
                elif obj.mode == "OBJECT":
                    obj.location = vector_delta
        if obj.mode == 'EDIT':
            bmesh.update_edit_mesh(obj.data)
            bm.select_history.clear()
        return

    # --------------
    # Add New Vertex
    elif operation == "N":
        if not obj.mode == "EDIT":
            pg.error = PDT_ERR_ADDVEDIT
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        # Absolute/Global Coordinates
        if mode == "a":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            new_vertex = bm.verts.new(vector_delta - obj_loc)
        # Delta/Relative Coordinates
        elif mode == "d":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            new_vertex = bm.verts.new(verts[-1].co + vector_delta)
        # Direction/Polar Coordinates
        elif mode == "i":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
            if not valid_result:
                return
            new_vertex = bm.verts.new(verts[-1].co + vector_delta)
        # Percent Options
        elif mode == "p":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
            if not valid_result:
                return
            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()
        return

    # -----------
    # Split Edges
    elif operation == "S":
        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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            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
            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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            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
            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)]
            bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
        # Directional/Polar Coordinates
        elif mode == "i":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
            if not valid_result:
                return
            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
            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)]
            bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
        # Percent Options
        elif mode == "p":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
            if not valid_result:
                return
            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
            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()
        return

    # ----------------
    # Extrude Vertices
    elif operation == "V":
        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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            new_vertex = bm.verts.new(vector_delta - obj_loc)
            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
            )
        # Delta/Relative Coordinates
        elif mode == "d":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
            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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
            if not valid_result:
                return
            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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 1)
            if not valid_result:
                return
            verts = [v for v in bm.verts if v.select].copy()
            if len(verts) == 0:
                pg.error = PDT_ERR_NO_SEL_GEOM
                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
                return
            new_vertex = bm.verts.new(vector_delta)
            if ext_a:
                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)
        bm.select_history.clear()
        return

    # ----------------
    # Extrude Geometry
    elif operation == "E":
        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":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
        # Direction/Polar Coordinates
        elif mode == "i":
            valid_result, vector_delta = vector_build(context, pg, obj, values, 2)
            if not valid_result:
                return

        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
        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()
        return

    # ------------------
    # Duplicate Geometry
    elif operation == "D":
        if not obj.mode == "EDIT":
            pg.error = PDT_ERR_DUPEDIT
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        # Delta/Relative Coordinates
        if mode == "d":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 3)
            if not valid_result:
                return
        # Direction/Polar Coordinates
        elif mode == "i":
            valid_result, vector_delta = vector_build(context, pg, obj, operation, values, 2)
            if not valid_result:
                return

        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
        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)
        bm.select_history.clear()
        return

    # ---------------
    # Fillet Geometry
    elif operation == "F":
        if obj is None:
            pg.error = PDT_ERR_NO_ACT_OBJ
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        if not obj.mode == "EDIT":
            pg.error = PDT_ERR_FILEDIT
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        if len(values) != 3:
            pg.error = PDT_ERR_BAD3VALS
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        if mode in {"i", "v"}:
            vert_bool = True
        elif mode == "e":
            vert_bool = False
        verts = [v for v in bm.verts if v.select]
        if len(verts) == 0:
            pg.error = PDT_ERR_SEL_1_VERT
            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
            return
        # 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])
        _segments = int(values[1])
        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
            edges = [e for e in bm.edges if e.select]
            if len(edges) == 2 and len(verts) == 4:
                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
                    )
                if not done:
                    errmsg = f"{PDT_ERR_INT_LINES} {plane}  {PDT_LAB_PLANE}"
                    self.report({"ERROR"}, errmsg)
                    return {"FINISHED"}
                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)
        bpy.ops.mesh.bevel(
            offset_type="OFFSET",
            offset=_offset,
            segments=_segments,
            profile=_profile,
            vertex_only=vert_bool
        )
        return