1# ***** BEGIN GPL LICENSE BLOCK *****
2#
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software Foundation,
16# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17#
18# ***** END GPL LICENCE BLOCK *****
19#
20# -----------------------------------------------------------------------
21# Author: Alan Odom (Clockmender), Rune Morling (ermo) Copyright (c) 2019
22# -----------------------------------------------------------------------
23#
24import bpy
25import bmesh
26import math
27from bpy.types import Operator
28from mathutils import Vector
29from .pdt_functions import (
30    debug,
31    intersection,
32    obj_check,
33    oops,
34    update_sel,
35    view_coords,
36    view_dir,
37)
38from .pdt_command_functions import (
39    vector_build,
40    join_two_vertices,
41    set_angle_distance_two,
42    set_angle_distance_three,
43    origin_to_cursor,
44    taper,
45    placement_normal,
46    placement_arc_centre,
47    placement_intersect,
48)
49from .pdt_msg_strings import (
50    PDT_ERR_ADDVEDIT,
51    PDT_ERR_BADFLETTER,
52    PDT_ERR_CHARS_NUM,
53    PDT_ERR_DUPEDIT,
54    PDT_ERR_EXTEDIT,
55    PDT_ERR_FACE_SEL,
56    PDT_ERR_FILEDIT,
57    PDT_ERR_NON_VALID,
58    PDT_ERR_NO_SEL_GEOM,
59    PDT_ERR_SEL_1_EDGE,
60    PDT_ERR_SEL_1_EDGEM,
61    PDT_ERR_SPLITEDIT,
62    PDT_ERR_BADMATHS,
63    PDT_OBJ_MODE_ERROR,
64    PDT_ERR_SEL_4_VERTS,
65    PDT_ERR_INT_LINES,
66    PDT_LAB_PLANE,
67    PDT_ERR_NO_ACT_OBJ,
68    PDT_ERR_VERT_MODE,
69)
70from .pdt_bix import add_line_to_bisection
71from .pdt_etof import extend_vertex
72from .pdt_xall import intersect_all
73
74from . import pdt_exception
75PDT_SelectionError = pdt_exception.SelectionError
76PDT_InvalidVector = pdt_exception.InvalidVector
77PDT_CommandFailure = pdt_exception.CommandFailure
78PDT_ObjectModeError = pdt_exception.ObjectModeError
79PDT_MathsError = pdt_exception.MathsError
80PDT_IntersectionError = pdt_exception.IntersectionError
81PDT_NoObjectError = pdt_exception.NoObjectError
82PDT_FeatureError = pdt_exception.FeatureError
83
84
85class PDT_OT_CommandReRun(Operator):
86    """Repeat Current Displayed Command."""
87
88    bl_idname = "pdt.command_rerun"
89    bl_label = "Re-run Current Command"
90    bl_options = {"REGISTER", "UNDO"}
91
92    def execute(self, context):
93        """Repeat Current Command Line Input.
94
95        Args:
96            context: Blender bpy.context instance.
97
98        Returns:
99            Nothing.
100        """
101        command_run(self, context)
102        return {"FINISHED"}
103
104
105def command_run(self, context):
106    """Run Command String as input into Command Line.
107
108    Note:
109        Uses pg.command, pg.error & many other 'pg.' variables to set PDT menu items,
110        or alter functions
111
112        Command Format; Operation(single letter) Mode(single letter) Values(up to 3 values
113        separated by commas)
114
115        Example; CD0.4,0.6,1.1 - Moves Cursor Delta XYZ = 0.4,0.6,1.1 from Current Position/Active
116        Vertex/Object Origin
117
118        Example; SP35 - Splits active Edge at 35% of separation between edge's vertices
119
120        Valid First Letters (as 'operation' - pg.command[0])
121            C = Cursor, G = Grab(move), N = New Vertex, V = Extrude Vertices Only,
122            E = Extrude geometry, P = Move Pivot Point, D = Duplicate geometry, S = Split Edges
123
124            Capitals and lower case letters are both allowed
125
126        Valid Second Letters (as 'mode' - pg.command[1])
127
128            A = Absolute XYZ, D = Delta XYZ, I = Distance at Angle, P = Percent
129            X = X Delta, Y = Y, Delta Z, = Z Delta, O = Output (Maths Operation only)
130            V = Vertex Bevel, E = Edge Bevel, I = Intersect then Bevel
131
132            Capitals and lower case letters are both allowed
133
134        Valid Values (pdt_command[2:])
135            Only Integers and Floats, missing values are set to 0, appropriate length checks are
136            performed as Values is split by commas.
137
138            Example; CA,,3 - Cursor to Absolute, is re-interpreted as CA0,0,3
139
140            Exception for Maths Operation, Values section is evaluated as Maths expression
141
142            Example; madegrees(atan(3/4)) - sets PDT Angle to smallest angle of 3,4,5 Triangle;
143            (36.8699 degrees)
144
145    Args:
146        context: Blender bpy.context instance.
147
148    Returns:
149        Nothing.
150    """
151
152    scene = context.scene
153    pg = scene.pdt_pg
154    command = pg.command.strip()
155
156    # Check Object Type & Mode First
157    obj = context.view_layer.objects.active
158    if obj is not None and command[0].upper() not in {"M", "?", "HELP"}:
159        if obj.mode not in {"OBJECT", "EDIT"} or obj.type not in {"MESH", "EMPTY"}:
160            pg.error = PDT_OBJ_MODE_ERROR
161            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
162            raise PDT_ObjectModeError
163
164    # Special Cases of Command.
165    if command == "?" or command.lower() == "help":
166        # fmt: off
167        context.window_manager.popup_menu(pdt_help, title="PDT Command Line Help", icon="INFO")
168        # fmt: on
169        return
170    if command == "":
171        return
172    if command.upper() == "J2V":
173        join_two_vertices(context)
174        return
175    if command.upper() == "AD2":
176        set_angle_distance_two(context)
177        return
178    if command.upper() == "AD3":
179        set_angle_distance_three(context)
180        return
181    if command.upper() == "OTC":
182        origin_to_cursor(context)
183        return
184    if command.upper() == "TAP":
185        taper(context)
186        return
187    if command.upper() == "BIS":
188        add_line_to_bisection(context)
189        return
190    if command.upper() == "ETF":
191        extend_vertex(context)
192        return
193    if command.upper() == "INTALL":
194        intersect_all(context)
195        return
196    if command.upper()[1:] == "NML":
197        placement_normal(context, command.upper()[0])
198        return
199    if command.upper()[1:] == "CEN":
200        placement_arc_centre(context, command.upper()[0])
201        return
202    if command.upper()[1:] == "INT":
203        placement_intersect(context, command.upper()[0])
204        return
205
206    # Check Command Length
207    if len(command) < 3:
208        pg.error = PDT_ERR_CHARS_NUM
209        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
210        return
211
212    # Check First Letter
213    operation = command[0].upper()
214    if operation not in {"C", "D", "E", "F", "G", "N", "M", "P", "V", "S"}:
215        pg.error = PDT_ERR_BADFLETTER
216        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
217        return
218
219    # Check Second Letter.
220    mode = command[1].lower()
221    if (
222            (operation == "F" and mode not in {"v", "e", "i"})
223            or (operation in {"D", "E"} and mode not in {"d", "i"})
224            or (operation == "M" and mode not in {"a", "d", "i", "p", "o", "x", "y", "z"})
225            or (operation not in {"D", "E", "F", "M"} and mode not in {"a", "d", "i", "p"})
226        ):
227        pg.error = f"'{mode}' {PDT_ERR_NON_VALID} '{operation}'"
228        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
229        return
230
231    # --------------
232    # Maths Operation
233    if operation == "M":
234        try:
235            command_maths(context, mode, pg, command[2:], mode)
236            return
237        except PDT_MathsError:
238            return
239
240    # -----------------------------------------------------
241    # Not a Maths Operation, so let's parse the command line
242    try:
243        pg, values, obj, obj_loc, bm, verts = command_parse(context)
244    except PDT_SelectionError:
245        return
246
247    # ---------------------
248    # Cursor or Pivot Point
249    if operation in {"C", "P"}:
250        try:
251            move_cursor_pivot(context, pg, operation, mode, obj, verts, values)
252        except PDT_CommandFailure:
253            return
254
255    # ------------------------
256    # Move Vertices or Objects
257    if operation == "G":
258        try:
259            move_entities(context, pg, operation, mode, obj, bm, verts, values)
260        except PDT_CommandFailure:
261            return
262
263    # --------------
264    # Add New Vertex
265    if operation == "N":
266        try:
267            add_new_vertex(context, pg, operation, mode, obj, bm, verts, values)
268        except PDT_CommandFailure:
269            return
270
271    # -----------
272    # Split Edges
273    if operation == "S":
274        try:
275            split_edges(context, pg, operation, mode, obj, obj_loc, bm, values)
276        except PDT_CommandFailure:
277            return
278
279
280    # ----------------
281    # Extrude Vertices
282    if operation == "V":
283        try:
284            extrude_vertices(context, pg, operation, mode, obj, obj_loc, bm, verts, values)
285        except PDT_CommandFailure:
286            return
287
288    # ----------------
289    # Extrude Geometry
290    if operation == "E":
291        try:
292            extrude_geometry(context, pg, operation, mode, obj, bm, values)
293        except PDT_CommandFailure:
294            return
295
296    # ------------------
297    # Duplicate Geometry
298    if operation == "D":
299        try:
300            duplicate_geometry(context, pg, operation, mode, obj, bm, values)
301        except PDT_CommandFailure:
302            return
303
304    # ---------------
305    # Fillet Geometry
306    if operation == "F":
307        try:
308            fillet_geometry(context, pg, mode, obj, bm, verts, values)
309        except PDT_CommandFailure:
310            return
311
312
313def pdt_help(self, context):
314    """Display PDT Command Line help in a pop-up.
315
316    Args:
317        context: Blender bpy.context instance
318
319    Returns:
320        Nothing.
321    """
322    label = self.layout.label
323    label(text="Primary Letters (Available Secondary Letters):")
324    label(text="")
325    label(text="C: Cursor (a, d, i, p)")
326    label(text="D: Duplicate Geometry (d, i)")
327    label(text="E: Extrude Geometry (d, i)")
328    label(text="F: Fillet (v, e, i)")
329    label(text="G: Grab (Move) (a, d, i, p)")
330    label(text="N: New Vertex (a, d, i, p)")
331    label(text="M: Maths Functions (a, d, p, o, x, y, z)")
332    label(text="P: Pivot Point (a, d, i, p)")
333    label(text="V: Extrude Vertice Only (a, d, i, p)")
334    label(text="S: Split Edges (a, d, i, p)")
335    label(text="?: Quick Help")
336    label(text="")
337    label(text="Secondary Letters:")
338    label(text="")
339    label(text="- General Options:")
340    label(text="a: Absolute (Global) Coordinates e.g. 1,3,2")
341    label(text="d: Delta (Relative) Coordinates, e.g. 0.5,0,1.2")
342    label(text="i: Directional (Polar) Coordinates e.g. 2.6,45")
343    label(text="p: Percent e.g. 67.5")
344    label(text="- Fillet Options:")
345    label(text="v: Fillet Vertices")
346    label(text="e: Fillet Edges")
347    label(text="i: Fillet & Intersect 2 Disconnected Edges")
348    label(text="- Math Options:")
349    label(text="x, y, z: Send result to X, Y and Z input fields in PDT Design")
350    label(text="d, a, p: Send result to Distance, Angle or Percent input field in PDT Design")
351    label(text="o: Send Maths Calculation to Output")
352    label(text="")
353    label(text="Note that commands are case-insensitive: ED = Ed = eD = ed")
354    label(text="")
355    label(text="Examples:")
356    label(text="")
357    label(text="ed0.5,,0.6")
358    label(text="'- Extrude Geometry Delta 0.5 in X, 0 in Y, 0.6 in Z")
359    label(text="")
360    label(text="fe0.1,4,0.5")
361    label(text="'- Fillet Edges")
362    label(text="'- Radius: 0.1 (float) -- the radius (or offset) of the bevel/fillet")
363    label(text="'- Segments: 4 (int) -- choosing an even amount of segments gives better geometry")
364    label(text="'- Profile: 0.5 (float[0.0;1.0]) -- 0.5 (default) yields a circular, convex shape")
365    label(text="")
366    label(text="More Information at:")
367    label(text="https://github.com/Clockmender/Precision-Drawing-Tools/wiki")
368
369
370def command_maths(context, mode, pg, expression, output_target):
371    """Evaluates Maths Input.
372
373    Args:
374        context: Blender bpy.context instance.
375        mode: The Operation Mode, e.g. a for Absolute
376        pg: PDT Parameters Group - our variables
377        expression: The Maths component of the command input e.g. sqrt(56)
378        output_target: The output variable box on the UI
379
380    Returns:
381        Nothing.
382    """
383
384    namespace = {}
385    namespace.update(vars(math))
386    try:
387        maths_result = eval(expression, namespace, namespace)
388    except:
389        pg.error = PDT_ERR_BADMATHS
390        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
391        raise PDT_MathsError
392
393    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
394    if output_target == "x":
395        pg.cartesian_coords.x = round(maths_result, decimal_places)
396    elif output_target == "y":
397        pg.cartesian_coords.y = round(maths_result, decimal_places)
398    elif output_target == "z":
399        pg.cartesian_coords.z = round(maths_result, decimal_places)
400    elif output_target == "d":
401        pg.distance = round(maths_result, decimal_places)
402    elif output_target == "a":
403        pg.angle = round(maths_result, decimal_places)
404    elif output_target == "p":
405        pg.percent = round(maths_result, decimal_places)
406    else:
407        # Must be "o"
408        pg.maths_output = round(maths_result, decimal_places)
409
410
411def command_parse(context):
412    """Parse Command Input.
413
414    Args:
415        context: Blender bpy.context instance.
416
417    Returns:
418        pg: PDT Parameters Group - our variables
419        values_out: The Output Values as a list of numbers
420        obj: The Active Object
421        obj_loc: The object's location in 3D space
422        bm: The object's Bmesh
423        verts: The object's selected vertices, or selected history vertices.
424    """
425    scene = context.scene
426    pg = scene.pdt_pg
427    command = pg.command.strip()
428    operation = command[0].upper()
429    mode = command[1].lower()
430    values = command[2:].split(",")
431    mode_sel = pg.select
432    obj = context.view_layer.objects.active
433    ind = 0
434    for v in values:
435        try:
436            _ = float(v)
437            good = True
438        except ValueError:
439            values[ind] = "0.0"
440        ind = ind + 1
441    # Apply System Rounding
442    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
443    values_out = [str(round(float(v), decimal_places)) for v in values]
444    bm = "No Bmesh"
445    obj_loc = Vector((0,0,0))
446    verts = []
447
448    if mode_sel == 'REL' and operation not in {"C", "P"}:
449        pg.select = 'SEL'
450        mode_sel = 'SEL'
451
452    if mode == "a" and operation not in {"C", "P"}:
453        # Place new Vetex, or Extrude Vertices by Absolute Coords.
454        if mode_sel == 'REL':
455            pg.select = 'SEL'
456            mode_sel = 'SEL'
457        if obj is not None:
458            if obj.mode == "EDIT":
459                bm = bmesh.from_edit_mesh(obj.data)
460                obj_loc = obj.matrix_world.decompose()[0]
461                verts = []
462            else:
463                if operation != "G":
464                    pg.error = PDT_OBJ_MODE_ERROR
465                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
466                    raise PDT_ObjectModeError
467        else:
468            pg.error = PDT_ERR_NO_ACT_OBJ
469            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
470            raise PDT_NoObjectError
471
472    if mode_sel == 'SEL' and mode not in {"a"}:
473        # All other options except Cursor or Pivot by Absolute
474        # These options require no object, etc.
475        bm, good = obj_check(obj, scene, operation)
476        if good and obj.mode == 'EDIT':
477            obj_loc = obj.matrix_world.decompose()[0]
478            if len(bm.select_history) == 0 or operation == "G":
479                verts = [v for v in bm.verts if v.select]
480                if len(verts) == 0:
481                    pg.error = PDT_ERR_NO_SEL_GEOM
482                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
483                    raise PDT_SelectionError
484            else:
485                verts = bm.select_history
486
487    debug(f"command: {operation}{mode}{values_out}")
488    debug(f"obj: {obj}, bm: {bm}, obj_loc: {obj_loc}")
489
490    return pg, values_out, obj, obj_loc, bm, verts
491
492
493def move_cursor_pivot(context, pg, operation, mode, obj, verts, values):
494    """Moves Cursor & Pivot Point.
495
496    Args:
497        context: Blender bpy.context instance.
498        pg: PDT Parameters Group - our variables
499        operation: The Operation e.g. Create New Vertex
500        mode: The Operation Mode, e.g. a for Absolute
501        obj: The Active Object
502        verts: The object's selected vertices, or selected history vertices
503        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
504
505    Returns:
506        Nothing.
507    """
508
509    # Absolute/Global Coordinates, or Delta/Relative Coordinates
510    if mode in {"a", "d"}:
511        try:
512            vector_delta = vector_build(context, pg, obj, operation, values, 3)
513        except:
514            raise PDT_InvalidVector
515    # Direction/Polar Coordinates
516    elif mode == "i":
517        try:
518            vector_delta = vector_build(context, pg, obj, operation, values, 2)
519        except:
520            raise PDT_InvalidVector
521    # Percent Options
522    else:
523        # Must be Percent
524        try:
525            vector_delta = vector_build(context, pg, obj, operation, values, 1)
526        except:
527            raise PDT_InvalidVector
528
529    scene = context.scene
530    mode_sel = pg.select
531    obj_loc = Vector((0,0,0))
532    if obj is not None:
533        obj_loc = obj.matrix_world.decompose()[0]
534
535    if mode == "a":
536        if operation == "C":
537            scene.cursor.location = vector_delta
538        elif operation == "P":
539            pg.pivot_loc = vector_delta
540    elif mode in {"d", "i"}:
541        if pg.plane == "LO" and mode == "d":
542            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
543        elif pg.plane == "LO" and mode == "i":
544            vector_delta = view_dir(pg.distance, pg.angle)
545        if mode_sel == "REL":
546            if operation == "C":
547                scene.cursor.location = scene.cursor.location + vector_delta
548            else:
549                pg.pivot_loc = pg.pivot_loc + vector_delta
550        elif mode_sel == "SEL":
551            if obj.mode == "EDIT":
552                if operation == "C":
553                    scene.cursor.location = verts[-1].co + obj_loc + vector_delta
554                else:
555                    pg.pivot_loc = verts[-1].co + obj_loc + vector_delta
556            if obj.mode == "OBJECT":
557                if operation == "C":
558                    scene.cursor.location = obj_loc + vector_delta
559                else:
560                    pg.pivot_loc = obj_loc + vector_delta
561    else:
562        # Must be Percent
563        if obj.mode == "EDIT":
564            if operation == "C":
565                scene.cursor.location = obj_loc + vector_delta
566            else:
567                pg.pivot_loc = obj_loc + vector_delta
568        if obj.mode == "OBJECT":
569            if operation == "C":
570                scene.cursor.location = vector_delta
571            else:
572                pg.pivot_loc = vector_delta
573
574
575def move_entities(context, pg, operation, mode, obj, bm, verts, values):
576    """Moves Entities.
577
578    Args:
579        context: Blender bpy.context instance.
580        pg: PDT Parameters Group - our variables
581        operation: The Operation e.g. Create New Vertex
582        mode: The Operation Mode, e.g. a for Absolute
583        obj: The Active Object
584        bm: The object's Bmesh
585        verts: The object's selected vertices, or selected history vertices
586        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
587
588    Returns:
589        Nothing.
590    """
591
592    obj_loc = obj.matrix_world.decompose()[0]
593
594    # Absolute/Global Coordinates
595    if mode == "a":
596        try:
597            vector_delta = vector_build(context, pg, obj, operation, values, 3)
598        except:
599            raise PDT_InvalidVector
600        if obj.mode == "EDIT":
601            for v in verts:
602                v.co = vector_delta - obj_loc
603            bmesh.ops.remove_doubles(
604                bm, verts=[v for v in bm.verts if v.select], dist=0.0001
605            )
606        if obj.mode == "OBJECT":
607            for ob in context.view_layer.objects.selected:
608                ob.location = vector_delta
609
610    elif mode in {"d", "i"}:
611        if mode == "d":
612            # Delta/Relative Coordinates
613            try:
614                vector_delta = vector_build(context, pg, obj, operation, values, 3)
615            except:
616                raise PDT_InvalidVector
617        else:
618            # Direction/Polar Coordinates
619            try:
620                vector_delta = vector_build(context, pg, obj, operation, values, 2)
621            except:
622                raise PDT_InvalidVector
623
624        if pg.plane == "LO" and mode == "d":
625            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
626        elif pg.plane == "LO" and mode == "i":
627            vector_delta = view_dir(pg.distance, pg.angle)
628
629        if obj.mode == "EDIT":
630            bmesh.ops.translate(
631                bm, verts=[v for v in bm.verts if v.select], vec=vector_delta
632            )
633        if obj.mode == "OBJECT":
634            for ob in context.view_layer.objects.selected:
635                ob.location = obj_loc + vector_delta
636    # Percent Options Only Other Choice
637    else:
638        try:
639            vector_delta = vector_build(context, pg, obj, operation, values, 1)
640        except:
641            raise PDT_InvalidVector
642        if obj.mode == 'EDIT':
643            verts[-1].co = vector_delta
644        if obj.mode == "OBJECT":
645            obj.location = vector_delta
646    if obj.mode == 'EDIT':
647        bmesh.update_edit_mesh(obj.data)
648        bm.select_history.clear()
649
650
651def add_new_vertex(context, pg, operation, mode, obj, bm, verts, values):
652    """Add New Vertex.
653
654    Args:
655        context: Blender bpy.context instance.
656        pg, operation, mode, obj, bm, verts, values
657
658    Returns:
659        Nothing.
660    """
661
662    obj_loc = obj.matrix_world.decompose()[0]
663
664    if not obj.mode == "EDIT":
665        pg.error = PDT_ERR_ADDVEDIT
666        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
667        raise PDT_SelectionError
668    if mode not in {"a"}:
669        if not isinstance(verts[0], bmesh.types.BMVert):
670            pg.error = PDT_ERR_VERT_MODE
671            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
672            raise PDT_FeatureError
673    # Absolute/Global Coordinates
674    if mode == "a":
675        try:
676            vector_delta = vector_build(context, pg, obj, operation, values, 3)
677        except:
678            raise PDT_InvalidVector
679        new_vertex = bm.verts.new(vector_delta - obj_loc)
680    # Delta/Relative Coordinates
681    elif mode == "d":
682        try:
683            vector_delta = vector_build(context, pg, obj, operation, values, 3)
684        except:
685            raise PDT_InvalidVector
686        if pg.plane == "LO":
687            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
688        new_vertex = bm.verts.new(verts[-1].co + vector_delta)
689    # Direction/Polar Coordinates
690    elif mode == "i":
691        try:
692            vector_delta = vector_build(context, pg, obj, operation, values, 2)
693        except:
694            raise PDT_InvalidVector
695        if pg.plane == "LO":
696            vector_delta = view_dir(pg.distance, pg.angle)
697        new_vertex = bm.verts.new(verts[-1].co + vector_delta)
698    # Percent Options Only Other Choice
699    else:
700        try:
701            vector_delta = vector_build(context, pg, obj, operation, values, 1)
702        except:
703            raise PDT_InvalidVector
704        new_vertex = bm.verts.new(vector_delta)
705
706    for v in [v for v in bm.verts if v.select]:
707        v.select_set(False)
708    new_vertex.select_set(True)
709    bmesh.update_edit_mesh(obj.data)
710    bm.select_history.clear()
711
712
713def split_edges(context, pg, operation, mode, obj, obj_loc, bm, values):
714    """Split Edges.
715
716    Args:
717        context: Blender bpy.context instance.
718        pg: PDT Parameters Group - our variables
719        operation: The Operation e.g. Create New Vertex
720        mode: The Operation Mode, e.g. a for Absolute
721        obj: The Active Object
722        obj_loc: The object's location in 3D space
723        bm: The object's Bmesh
724        verts: The object's selected vertices, or selected history vertices
725        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
726
727    Returns:
728        Nothing.
729    """
730
731    if not obj.mode == "EDIT":
732        pg.error = PDT_ERR_SPLITEDIT
733        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
734        return
735    # Absolute/Global Coordinates
736    if mode == "a":
737        try:
738            vector_delta = vector_build(context, pg, obj, operation, values, 3)
739        except:
740            raise PDT_InvalidVector
741        edges = [e for e in bm.edges if e.select]
742        if len(edges) != 1:
743            pg.error = f"{PDT_ERR_SEL_1_EDGE} {len(edges)})"
744            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
745            return
746        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
747        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
748        new_vertex = new_verts[0]
749        new_vertex.co = vector_delta - obj_loc
750    # Delta/Relative Coordinates
751    elif mode == "d":
752        try:
753            vector_delta = vector_build(context, pg, obj, operation, values, 3)
754        except:
755            raise PDT_InvalidVector
756        edges = [e for e in bm.edges if e.select]
757        faces = [f for f in bm.faces if f.select]
758        if len(faces) != 0:
759            pg.error = PDT_ERR_FACE_SEL
760            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
761            return
762        if len(edges) < 1:
763            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
764            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
765            return
766        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
767        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
768        bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
769    # Directional/Polar Coordinates
770    elif mode == "i":
771        try:
772            vector_delta = vector_build(context, pg, obj, operation, values, 2)
773        except:
774            raise PDT_InvalidVector
775        edges = [e for e in bm.edges if e.select]
776        faces = [f for f in bm.faces if f.select]
777        if len(faces) != 0:
778            pg.error = PDT_ERR_FACE_SEL
779            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
780            return
781        if len(edges) < 1:
782            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
783            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
784            return
785        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
786        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
787        bmesh.ops.translate(bm, verts=new_verts, vec=vector_delta)
788    # Percent Options
789    elif mode == "p":
790        try:
791            vector_delta = vector_build(context, pg, obj, operation, values, 1)
792        except:
793            raise PDT_InvalidVector
794        edges = [e for e in bm.edges if e.select]
795        faces = [f for f in bm.faces if f.select]
796        if len(faces) != 0:
797            pg.error = PDT_ERR_FACE_SEL
798            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
799            return
800        if len(edges) != 1:
801            pg.error = f"{PDT_ERR_SEL_1_EDGEM} {len(edges)})"
802            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
803            return
804        geom = bmesh.ops.subdivide_edges(bm, edges=edges, cuts=1)
805        new_verts = [v for v in geom["geom_split"] if isinstance(v, bmesh.types.BMVert)]
806        new_vertex = new_verts[0]
807        new_vertex.co = vector_delta
808
809    for v in [v for v in bm.verts if v.select]:
810        v.select_set(False)
811    for v in new_verts:
812        v.select_set(False)
813    bmesh.update_edit_mesh(obj.data)
814    bm.select_history.clear()
815
816
817def extrude_vertices(context, pg, operation, mode, obj, obj_loc, bm, verts, values):
818    """Extrude Vertices.
819
820    Args:
821        context: Blender bpy.context instance.
822        pg: PDT Parameters Group - our variables
823        operation: The Operation e.g. Create New Vertex
824        mode: The Operation Mode, e.g. a for Absolute
825        obj: The Active Object
826        obj_loc: The object's location in 3D space
827        bm: The object's Bmesh
828        verts: The object's selected vertices, or selected history vertices
829        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
830
831    Returns:
832        Nothing.
833    """
834
835    if not obj.mode == "EDIT":
836        pg.error = PDT_ERR_EXTEDIT
837        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
838        return
839    # Absolute/Global Coordinates
840    if mode == "a":
841        try:
842            vector_delta = vector_build(context, pg, obj, operation, values, 3)
843        except:
844            raise PDT_InvalidVector
845        new_vertex = bm.verts.new(vector_delta - obj_loc)
846        verts = [v for v in bm.verts if v.select].copy()
847        for v in verts:
848            bm.edges.new([v, new_vertex])
849            v.select_set(False)
850        new_vertex.select_set(True)
851        bmesh.ops.remove_doubles(
852            bm, verts=[v for v in bm.verts if v.select], dist=0.0001
853        )
854    # Delta/Relative Coordinates
855    elif mode == "d":
856        try:
857            vector_delta = vector_build(context, pg, obj, operation, values, 3)
858        except:
859            raise PDT_InvalidVector
860        if pg.plane == "LO":
861            vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
862        for v in verts:
863            new_vertex = bm.verts.new(v.co)
864            new_vertex.co = new_vertex.co + vector_delta
865            bm.edges.new([v, new_vertex])
866            v.select_set(False)
867            new_vertex.select_set(True)
868    # Direction/Polar Coordinates
869    elif mode == "i":
870        try:
871            vector_delta = vector_build(context, pg, obj, operation, values, 2)
872        except:
873            raise PDT_InvalidVector
874        if pg.plane == "LO":
875            vector_delta = view_dir(pg.distance, pg.angle)
876        for v in verts:
877            new_vertex = bm.verts.new(v.co)
878            new_vertex.co = new_vertex.co + vector_delta
879            bm.edges.new([v, new_vertex])
880            v.select_set(False)
881            new_vertex.select_set(True)
882    # Percent Options
883    elif mode == "p":
884        extend_all  = pg.extend
885        try:
886            vector_delta = vector_build(context, pg, obj, operation, values, 1)
887        except:
888            raise PDT_InvalidVector
889        verts = [v for v in bm.verts if v.select].copy()
890        new_vertex = bm.verts.new(vector_delta)
891        if extend_all:
892            for v in [v for v in bm.verts if v.select]:
893                bm.edges.new([v, new_vertex])
894                v.select_set(False)
895        else:
896            bm.edges.new([verts[-1], new_vertex])
897        new_vertex.select_set(True)
898
899    bmesh.update_edit_mesh(obj.data)
900
901
902def extrude_geometry(context, pg, operation, mode, obj, bm, values):
903    """Extrude Geometry.
904
905    Args:
906        context: Blender bpy.context instance.
907        pg: PDT Parameters Group - our variables
908        operation: The Operation e.g. Create New Vertex
909        mode: The Operation Mode, e.g. a for Absolute
910        obj: The Active Object
911        bm: The object's Bmesh
912        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
913
914    Returns:
915        Nothing.
916    """
917
918    if not obj.mode == "EDIT":
919        pg.error = PDT_ERR_EXTEDIT
920        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
921        return
922    # Delta/Relative Coordinates
923    if mode == "d":
924        try:
925            vector_delta = vector_build(context, pg, obj, operation, values, 3)
926        except:
927            raise PDT_InvalidVector
928    # Direction/Polar Coordinates
929    elif mode == "i":
930        try:
931            vector_delta = vector_build(context, pg, obj, operation, values, 2)
932        except:
933            raise PDT_InvalidVector
934
935    ret = bmesh.ops.extrude_face_region(
936        bm,
937        geom=(
938            [f for f in bm.faces if f.select]
939            + [e for e in bm.edges if e.select]
940            + [v for v in bm.verts if v.select]
941        ),
942        use_select_history=True,
943    )
944    geom_extr = ret["geom"]
945    verts_extr = [v for v in geom_extr if isinstance(v, bmesh.types.BMVert)]
946    edges_extr = [e for e in geom_extr if isinstance(e, bmesh.types.BMEdge)]
947    faces_extr = [f for f in geom_extr if isinstance(f, bmesh.types.BMFace)]
948    del ret
949
950    if pg.plane == "LO" and mode == "d":
951        vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
952    elif pg.plane == "LO" and mode == "i":
953        vector_delta = view_dir(pg.distance, pg.angle)
954
955    bmesh.ops.translate(bm, verts=verts_extr, vec=vector_delta)
956    update_sel(bm, verts_extr, edges_extr, faces_extr)
957    bmesh.update_edit_mesh(obj.data)
958    bm.select_history.clear()
959
960
961def duplicate_geometry(context, pg, operation, mode, obj, bm, values):
962    """Duplicate Geometry.
963
964    Args:
965        context: Blender bpy.context instance.
966        pg: PDT Parameters Group - our variables
967        operation: The Operation e.g. Create New Vertex
968        mode: The Operation Mode, e.g. a for Absolute
969        obj: The Active Object
970        bm: The object's Bmesh
971        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
972
973    Returns:
974        Nothing.
975    """
976
977    if not obj.mode == "EDIT":
978        pg.error = PDT_ERR_DUPEDIT
979        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
980        return
981    # Delta/Relative Coordinates
982    if mode == "d":
983        try:
984            vector_delta = vector_build(context, pg, obj, operation, values, 3)
985        except:
986            raise PDT_InvalidVector
987    # Direction/Polar Coordinates
988    elif mode == "i":
989        try:
990            vector_delta = vector_build(context, pg, obj, operation, values, 2)
991        except:
992            raise PDT_InvalidVector
993
994    ret = bmesh.ops.duplicate(
995        bm,
996        geom=(
997            [f for f in bm.faces if f.select]
998            + [e for e in bm.edges if e.select]
999            + [v for v in bm.verts if v.select]
1000        ),
1001        use_select_history=True,
1002    )
1003    geom_dupe = ret["geom"]
1004    verts_dupe = [v for v in geom_dupe if isinstance(v, bmesh.types.BMVert)]
1005    edges_dupe = [e for e in geom_dupe if isinstance(e, bmesh.types.BMEdge)]
1006    faces_dupe = [f for f in geom_dupe if isinstance(f, bmesh.types.BMFace)]
1007    del ret
1008
1009    if pg.plane == "LO" and mode == "d":
1010        vector_delta = view_coords(vector_delta.x, vector_delta.y, vector_delta.z)
1011    elif pg.plane == "LO" and mode == "i":
1012        vector_delta = view_dir(pg.distance, pg.angle)
1013
1014    bmesh.ops.translate(bm, verts=verts_dupe, vec=vector_delta)
1015    update_sel(bm, verts_dupe, edges_dupe, faces_dupe)
1016    bmesh.update_edit_mesh(obj.data)
1017
1018
1019def fillet_geometry(context, pg, mode, obj, bm, verts, values):
1020    """Fillet Geometry.
1021
1022    Args:
1023        context: Blender bpy.context instance.
1024        pg: PDT Parameters Group - our variables
1025        operation: The Operation e.g. Create New Vertex
1026        mode: The Operation Mode, e.g. a for Absolute
1027        obj: The Active Object
1028        bm: The object's Bmesh
1029        verts: The object's selected vertices, or selected history vertices
1030        values: The parameters passed e.g. 1,4,3 for Cartesian Coordinates
1031
1032    Returns:
1033        Nothing.
1034    """
1035
1036    if not obj.mode == "EDIT":
1037        pg.error = PDT_ERR_FILEDIT
1038        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
1039        return
1040    if mode in {"i", "v"}:
1041        affect = 'VERTICES'
1042    else:
1043        # Must be "e"
1044        affect = 'EDGES'
1045    # Note that passing an empty parameter results in that parameter being seen as "0"
1046    # _offset <= 0 is ignored since a bevel/fillet radius must be > 0 to make sense
1047    _offset = float(values[0])
1048    _segments = float(values[1])
1049    if _segments < 1:
1050        _segments = 1   # This is a single, flat segment (ignores profile)
1051    _profile = float(values[2])
1052    if _profile < 0.0 or _profile > 1.0:
1053        _profile = 0.5  # This is a circular profile
1054    if mode == "i":
1055        # Fillet & Intersect Two Edges
1056        # Always use Current Selection
1057        verts = [v for v in bm.verts if v.select]
1058        edges = [e for e in bm.edges if e.select]
1059        if len(edges) == 2 and len(verts) == 4:
1060            plane = pg.plane
1061            v_active = edges[0].verts[0]
1062            v_other = edges[0].verts[1]
1063            v_last = edges[1].verts[0]
1064            v_first = edges[1].verts[1]
1065            vector_delta, done = intersection(v_active.co,
1066                                              v_other.co,
1067                                              v_last.co,
1068                                              v_first.co,
1069                                              plane
1070                                              )
1071            if not done:
1072                pg.error = f"{PDT_ERR_INT_LINES} {plane}  {PDT_LAB_PLANE}"
1073                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
1074                raise PDT_IntersectionError
1075            if (v_active.co - vector_delta).length < (v_other.co - vector_delta).length:
1076                v_active.co = vector_delta
1077                v_other.select_set(False)
1078            else:
1079                v_other.co = vector_delta
1080                v_active.select_set(False)
1081            if (v_last.co - vector_delta).length < (v_first.co - vector_delta).length:
1082                v_last.co = vector_delta
1083                v_first.select_set(False)
1084            else:
1085                v_first.co = vector_delta
1086                v_last.select_set(False)
1087            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
1088        else:
1089            pg.error = f"{PDT_ERR_SEL_4_VERTS} {len(verts)} Vert(s), {len(edges)} Edge(s))"
1090            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
1091            raise PDT_SelectionError
1092
1093    bpy.ops.mesh.bevel(
1094        offset_type="OFFSET",
1095        offset=_offset,
1096        segments=_segments,
1097        profile=_profile,
1098        affect=affect
1099    )
1100