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 bmesh
25import numpy as np
26from math import sqrt, tan, pi
27from mathutils import Vector
28from mathutils.geometry import intersect_point_line
29from .pdt_functions import (
30    set_mode,
31    oops,
32    get_percent,
33    dis_ang,
34    check_selection,
35    arc_centre,
36    intersection,
37    view_coords_i,
38    view_coords,
39    view_dir,
40    set_axis,
41)
42
43from . import pdt_exception
44PDT_SelectionError = pdt_exception.SelectionError
45PDT_InvalidVector = pdt_exception.InvalidVector
46PDT_ObjectModeError = pdt_exception.ObjectModeError
47PDT_InfRadius = pdt_exception.InfRadius
48PDT_NoObjectError = pdt_exception.NoObjectError
49PDT_IntersectionError = pdt_exception.IntersectionError
50PDT_InvalidOperation = pdt_exception.InvalidOperation
51PDT_VerticesConnected = pdt_exception.VerticesConnected
52PDT_InvalidAngle = pdt_exception.InvalidAngle
53
54from .pdt_msg_strings import (
55    PDT_ERR_BAD3VALS,
56    PDT_ERR_BAD2VALS,
57    PDT_ERR_BAD1VALS,
58    PDT_ERR_CONNECTED,
59    PDT_ERR_SEL_2_VERTS,
60    PDT_ERR_EDOB_MODE,
61    PDT_ERR_NO_ACT_OBJ,
62    PDT_ERR_VERT_MODE,
63    PDT_ERR_SEL_3_VERTS,
64    PDT_ERR_SEL_3_OBJS,
65    PDT_ERR_EDIT_MODE,
66    PDT_ERR_NON_VALID,
67    PDT_LAB_NOR,
68    PDT_ERR_STRIGHT_LINE,
69    PDT_LAB_ARCCENTRE,
70    PDT_ERR_SEL_4_VERTS,
71    PDT_ERR_INT_NO_ALL,
72    PDT_LAB_INTERSECT,
73    PDT_ERR_SEL_4_OBJS,
74    PDT_INF_OBJ_MOVED,
75    PDT_ERR_SEL_2_VERTIO,
76    PDT_ERR_SEL_2_OBJS,
77    PDT_ERR_SEL_3_VERTIO,
78    PDT_ERR_TAPER_ANG,
79    PDT_ERR_TAPER_SEL,
80    PDT_ERR_INT_LINES,
81    PDT_LAB_PLANE,
82)
83
84
85def vector_build(context, pg, obj, operation, values, num_values):
86    """Build Movement Vector from Input Fields.
87
88    Args:
89        context: Blender bpy.context instance.
90        pg: PDT Parameters Group - our variables
91        obj: The Active Object
92        operation: The Operation e.g. Create New Vertex
93        values: The paramters passed e.g. 1,4,3 for Cartesian Coordinates
94        num_values: The number of values passed - determines the function
95
96    Returns:
97        Vector to position, or offset, items.
98    """
99
100    scene = context.scene
101    plane = pg.plane
102    flip_angle = pg.flip_angle
103    flip_percent= pg.flip_percent
104
105    # Cartesian 3D coordinates
106    if num_values == 3 and len(values) == 3:
107        output_vector = Vector((float(values[0]), float(values[1]), float(values[2])))
108    # Polar 2D coordinates
109    elif num_values == 2 and len(values) == 2:
110        output_vector = dis_ang(values, flip_angle, plane, scene)
111    # Percentage of imaginary line between two 3D coordinates
112    elif num_values == 1 and len(values) == 1:
113        output_vector = get_percent(obj, flip_percent, float(values[0]), operation, scene)
114    else:
115        if num_values == 3:
116            pg.error = PDT_ERR_BAD3VALS
117        elif num_values == 2:
118            pg.error = PDT_ERR_BAD2VALS
119        else:
120            pg.error = PDT_ERR_BAD1VALS
121        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
122        raise PDT_InvalidVector
123    return output_vector
124
125
126def placement_normal(context, operation):
127    """Manipulates Geometry, or Objects by Normal Intersection between 3 points.
128
129    Args:
130        context: Blender bpy.context instance.
131        operation: The Operation e.g. Create New Vertex
132
133    Returns:
134        Status Set.
135    """
136
137    scene = context.scene
138    pg = scene.pdt_pg
139    extend_all = pg.extend
140    obj = context.view_layer.objects.active
141
142    if obj.mode == "EDIT":
143        if obj is None:
144            pg.error = PDT_ERR_NO_ACT_OBJ
145            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
146            raise PDT_ObjectModeError
147        obj_loc = obj.matrix_world.decompose()[0]
148        bm = bmesh.from_edit_mesh(obj.data)
149        if len(bm.select_history) == 3:
150            vector_a, vector_b, vector_c = check_selection(3, bm, obj)
151            if vector_a is None:
152                pg.error = PDT_ERR_VERT_MODE
153                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
154                raise PDT_FeatureError
155        else:
156            pg.error = f"{PDT_ERR_SEL_3_VERTIO} {len(bm.select_history)})"
157            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
158            raise PDT_SelectionError
159    elif obj.mode == "OBJECT":
160        objs = context.view_layer.objects.selected
161        if len(objs) != 3:
162            pg.error = f"{PDT_ERR_SEL_3_OBJS} {len(objs)})"
163            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
164            raise PDT_SelectionError
165        objs_s = [ob for ob in objs if ob.name != obj.name]
166        vector_a = obj.matrix_world.decompose()[0]
167        vector_b = objs_s[-1].matrix_world.decompose()[0]
168        vector_c = objs_s[-2].matrix_world.decompose()[0]
169    vector_delta = intersect_point_line(vector_a, vector_b, vector_c)[0]
170    if operation == "C":
171        if obj.mode == "EDIT":
172            scene.cursor.location = obj_loc + vector_delta
173        elif obj.mode == "OBJECT":
174            scene.cursor.location = vector_delta
175    elif operation == "P":
176        if obj.mode == "EDIT":
177            pg.pivot_loc = obj_loc + vector_delta
178        elif obj.mode == "OBJECT":
179            pg.pivot_loc = vector_delta
180    elif operation == "G":
181        if obj.mode == "EDIT":
182            if extend_all:
183                for v in [v for v in bm.verts if v.select]:
184                    v.co = vector_delta
185                bm.select_history.clear()
186                bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts if v.select], dist=0.0001)
187            else:
188                bm.select_history[-1].co = vector_delta
189                bm.select_history.clear()
190            bmesh.update_edit_mesh(obj.data)
191        elif obj.mode == "OBJECT":
192            context.view_layer.objects.active.location = vector_delta
193    elif operation == "N":
194        if obj.mode == "EDIT":
195            vertex_new = bm.verts.new(vector_delta)
196            bmesh.update_edit_mesh(obj.data)
197            bm.select_history.clear()
198            for v in [v for v in bm.verts if v.select]:
199                v.select_set(False)
200            vertex_new.select_set(True)
201        else:
202            pg.error = f"{PDT_ERR_EDIT_MODE} {obj.mode})"
203            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
204            return
205    elif operation == "V" and obj.mode == "EDIT":
206        vector_new = vector_delta
207        vertex_new = bm.verts.new(vector_new)
208        if extend_all:
209            for v in [v for v in bm.verts if v.select]:
210                bm.edges.new([v, vertex_new])
211        else:
212            bm.edges.new([bm.select_history[-1], vertex_new])
213        for v in [v for v in bm.verts if v.select]:
214            v.select_set(False)
215        vertex_new.select_set(True)
216        bmesh.update_edit_mesh(obj.data)
217        bm.select_history.clear()
218    else:
219        pg.error = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_NOR}"
220        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
221
222
223def placement_arc_centre(context, operation):
224    """Manipulates Geometry, or Objects to an Arc Centre defined by 3 points on an Imaginary Arc.
225
226    Args:
227        context: Blender bpy.context instance.
228        operation: The Operation e.g. Create New Vertex
229
230    Returns:
231        Status Set.
232    """
233
234    scene = context.scene
235    pg = scene.pdt_pg
236    extend_all = pg.extend
237    obj = context.view_layer.objects.active
238
239    if obj.mode == "EDIT":
240        if obj is None:
241            pg.error = PDT_ERR_NO_ACT_OBJ
242            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
243            raise PDT_ObjectModeError
244        obj = context.view_layer.objects.active
245        obj_loc = obj.matrix_world.decompose()[0]
246        bm = bmesh.from_edit_mesh(obj.data)
247        verts = [v for v in bm.verts if v.select]
248        if len(verts) != 3:
249            pg.error = f"{PDT_ERR_SEL_3_VERTS} {len(verts)})"
250            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
251            raise PDT_SelectionError
252        vector_a = verts[0].co
253        vector_b = verts[1].co
254        vector_c = verts[2].co
255        vector_delta, radius = arc_centre(vector_a, vector_b, vector_c)
256        if str(radius) == "inf":
257            pg.error = PDT_ERR_STRIGHT_LINE
258            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
259            raise PDT_InfRadius
260        pg.distance = radius
261        if operation == "C":
262            scene.cursor.location = obj_loc + vector_delta
263        elif operation == "P":
264            pg.pivot_loc = obj_loc + vector_delta
265        elif operation == "N":
266            vector_new = vector_delta
267            vertex_new = bm.verts.new(vector_new)
268            for v in [v for v in bm.verts if v.select]:
269                v.select_set(False)
270            vertex_new.select_set(True)
271            bmesh.update_edit_mesh(obj.data)
272            bm.select_history.clear()
273            vertex_new.select_set(True)
274        elif operation == "G":
275            if extend_all:
276                for v in [v for v in bm.verts if v.select]:
277                    v.co = vector_delta
278                bm.select_history.clear()
279                bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts if v.select], dist=0.0001)
280            else:
281                bm.select_history[-1].co = vector_delta
282                bm.select_history.clear()
283            bmesh.update_edit_mesh(obj.data)
284        elif operation == "V":
285            vertex_new = bm.verts.new(vector_delta)
286            if extend_all:
287                for v in [v for v in bm.verts if v.select]:
288                    bm.edges.new([v, vertex_new])
289                    v.select_set(False)
290                vertex_new.select_set(True)
291                bm.select_history.clear()
292                bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts if v.select], dist=0.0001)
293                bmesh.update_edit_mesh(obj.data)
294            else:
295                bm.edges.new([bm.select_history[-1], vertex_new])
296                bmesh.update_edit_mesh(obj.data)
297                bm.select_history.clear()
298        else:
299            pg.error = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_ARCCENTRE}"
300            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
301    elif obj.mode == "OBJECT":
302        if len(context.view_layer.objects.selected) != 3:
303            pg.error = f"{PDT_ERR_SEL_3_OBJS} {len(context.view_layer.objects.selected)})"
304            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
305            raise PDT_SelectionError
306        vector_a = context.view_layer.objects.selected[0].matrix_world.decompose()[0]
307        vector_b = context.view_layer.objects.selected[1].matrix_world.decompose()[0]
308        vector_c = context.view_layer.objects.selected[2].matrix_world.decompose()[0]
309        vector_delta, radius = arc_centre(vector_a, vector_b, vector_c)
310        pg.distance = radius
311        if operation == "C":
312            scene.cursor.location = vector_delta
313        elif operation == "P":
314            pg.pivot_loc = vector_delta
315        elif operation == "G":
316            context.view_layer.objects.active.location = vector_delta
317        else:
318            pg.error = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_ARCCENTRE}"
319            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
320
321
322def placement_intersect(context, operation):
323    """Manipulates Geometry, or Objects by Convergance Intersection between 4 points, or 2 Edges.
324
325    Args:
326        context: Blender bpy.context instance.
327        operation: The Operation e.g. Create New Vertex
328
329    Returns:
330        Status Set.
331    """
332
333    scene = context.scene
334    pg = scene.pdt_pg
335    plane = pg.plane
336    obj = context.view_layer.objects.active
337    if obj.mode == "EDIT":
338        if obj is None:
339            pg.error = PDT_ERR_NO_ACT_OBJ
340            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
341            raise PDT_NoObjectError
342        obj_loc = obj.matrix_world.decompose()[0]
343        bm = bmesh.from_edit_mesh(obj.data)
344        edges = [e for e in bm.edges if e.select]
345        extend_all = pg.extend
346
347        if len(edges) == 2:
348            vertex_a = edges[0].verts[0]
349            vertex_b = edges[0].verts[1]
350            vertex_c = edges[1].verts[0]
351            vertex_d = edges[1].verts[1]
352        else:
353            if len(bm.select_history) != 4:
354                pg.error = (
355                    PDT_ERR_SEL_4_VERTS
356                    + str(len(bm.select_history))
357                    + " Vertices/"
358                    + str(len(edges))
359                    + " Edges)"
360                )
361                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
362                raise PDT_SelectionError
363            vertex_a = bm.select_history[-1]
364            vertex_b = bm.select_history[-2]
365            vertex_c = bm.select_history[-3]
366            vertex_d = bm.select_history[-4]
367
368        vector_delta, done = intersection(vertex_a.co, vertex_b.co, vertex_c.co, vertex_d.co, plane)
369        if not done:
370            pg.error = f"{PDT_ERR_INT_LINES} {plane}  {PDT_LAB_PLANE}"
371            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
372            raise PDT_IntersectionError
373
374        if operation == "C":
375            scene.cursor.location = obj_loc + vector_delta
376        elif operation == "P":
377            pg.pivot_loc = obj_loc + vector_delta
378        elif operation == "N":
379            vector_new = vector_delta
380            vertex_new = bm.verts.new(vector_new)
381            for v in [v for v in bm.verts if v.select]:
382                v.select_set(False)
383            for f in bm.faces:
384                f.select_set(False)
385            for e in bm.edges:
386                e.select_set(False)
387            vertex_new.select_set(True)
388            bmesh.update_edit_mesh(obj.data)
389            bm.select_history.clear()
390        elif operation in {"G", "V"}:
391            vertex_new = None
392            process = False
393
394            if (vertex_a.co - vector_delta).length < (vertex_b.co - vector_delta).length:
395                if operation == "G":
396                    vertex_a.co = vector_delta
397                    process = True
398                else:
399                    vertex_new = bm.verts.new(vector_delta)
400                    bm.edges.new([vertex_a, vertex_new])
401                    process = True
402            else:
403                if operation == "G" and extend_all:
404                    vertex_b.co = vector_delta
405                elif operation == "V" and extend_all:
406                    vertex_new = bm.verts.new(vector_delta)
407                    bm.edges.new([vertex_b, vertex_new])
408                else:
409                    return
410
411            if (vertex_c.co - vector_delta).length < (vertex_d.co - vector_delta).length:
412                if operation == "G" and extend_all:
413                    vertex_c.co = vector_delta
414                elif operation == "V" and extend_all:
415                    bm.edges.new([vertex_c, vertex_new])
416                else:
417                    return
418            else:
419                if operation == "G" and extend_all:
420                    vertex_d.co = vector_delta
421                elif operation == "V" and extend_all:
422                    bm.edges.new([vertex_d, vertex_new])
423                else:
424                    return
425            bm.select_history.clear()
426            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
427
428            if not process and not extend_all:
429                pg.error = PDT_ERR_INT_NO_ALL
430                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
431                bmesh.update_edit_mesh(obj.data)
432                return
433            for v in bm.verts:
434                v.select_set(False)
435            for f in bm.faces:
436                f.select_set(False)
437            for e in bm.edges:
438                e.select_set(False)
439
440            if vertex_new is not None:
441                vertex_new.select_set(True)
442            for v in bm.select_history:
443                if v is not None:
444                    v.select_set(True)
445            bmesh.update_edit_mesh(obj.data)
446        else:
447            pg.error = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}"
448            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
449            raise PDT_InvalidOperation
450
451    elif obj.mode == "OBJECT":
452        if len(context.view_layer.objects.selected) != 4:
453            pg.error = f"{PDT_ERR_SEL_4_OBJS} {len(context.view_layer.objects.selected)})"
454            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
455            raise PDT_SelectionError
456        order = pg.object_order.split(",")
457        objs = sorted(context.view_layer.objects.selected, key=lambda x: x.name)
458        pg.error = (
459            "Original Object Order (1,2,3,4) was: "
460            + objs[0].name
461            + ", "
462            + objs[1].name
463            + ", "
464            + objs[2].name
465            + ", "
466            + objs[3].name
467        )
468        context.window_manager.popup_menu(oops, title="Info", icon="INFO")
469
470        vector_a = objs[int(order[0]) - 1].matrix_world.decompose()[0]
471        vector_b = objs[int(order[1]) - 1].matrix_world.decompose()[0]
472        vector_c = objs[int(order[2]) - 1].matrix_world.decompose()[0]
473        vector_d = objs[int(order[3]) - 1].matrix_world.decompose()[0]
474        vector_delta, done = intersection(vector_a, vector_b, vector_c, vector_d, plane)
475        if not done:
476            pg.error = f"{PDT_ERR_INT_LINES} {plane}  {PDT_LAB_PLANE}"
477            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
478            raise PDT_IntersectionError
479        if operation == "C":
480            scene.cursor.location = vector_delta
481        elif operation == "P":
482            pg.pivot_loc = vector_delta
483        elif operation == "G":
484            context.view_layer.objects.active.location = vector_delta
485            pg.error = f"{PDT_INF_OBJ_MOVED} {context.view_layer.objects.active.name}"
486            context.window_manager.popup_menu(oops, title="Info", icon="INFO")
487        else:
488            pg.error = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}"
489            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
490        return
491    else:
492        return
493
494
495def join_two_vertices(context):
496    """Joins 2 Free Vertices that do not form part of a Face.
497
498    Note:
499        Joins two vertices that do not form part of a single face
500        It is designed to close open Edge Loops, where a face is not required
501        or to join two disconnected Edges.
502
503    Args:
504        context: Blender bpy.context instance.
505
506    Returns:
507        Status Set.
508    """
509
510    scene = context.scene
511    pg = scene.pdt_pg
512    obj = context.view_layer.objects.active
513    if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
514        bm = bmesh.from_edit_mesh(obj.data)
515        verts = [v for v in bm.verts if v.select]
516        if len(verts) == 2:
517            try:
518                bm.edges.new([verts[-1], verts[-2]])
519                bmesh.update_edit_mesh(obj.data)
520                bm.select_history.clear()
521                return
522            except ValueError:
523                pg.error = PDT_ERR_CONNECTED
524                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
525                raise PDT_VerticesConnected
526        else:
527            pg.error = f"{PDT_ERR_SEL_2_VERTS} {len(verts)})"
528            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
529            raise PDT_SelectionError
530    else:
531        pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
532        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
533        raise PDT_ObjectModeError
534
535
536def set_angle_distance_two(context):
537    """Measures Angle and Offsets between 2 Points in View Plane.
538
539    Note:
540        Uses 2 Selected Vertices to set pg.angle and pg.distance scene variables
541        also sets delta offset from these 2 points using standard Numpy Routines
542        Works in Edit and Oject Modes.
543
544    Args:
545        context: Blender bpy.context instance.
546
547    Returns:
548        Status Set.
549    """
550
551    scene = context.scene
552    pg = scene.pdt_pg
553    plane = pg.plane
554    flip_angle = pg.flip_angle
555    obj = context.view_layer.objects.active
556    if obj is None:
557        pg.error = PDT_ERR_NO_ACT_OBJ
558        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
559        return
560    if obj.mode == "EDIT":
561        bm = bmesh.from_edit_mesh(obj.data)
562        verts = [v for v in bm.verts if v.select]
563        if len(verts) == 2:
564            if len(bm.select_history) == 2:
565                vector_a, vector_b = check_selection(2, bm, obj)
566                if vector_a is None:
567                    pg.error = PDT_ERR_VERT_MODE
568                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
569                    raise PDT_FeatureError
570            else:
571                pg.error = f"{PDT_ERR_SEL_2_VERTIO} {len(bm.select_history)})"
572                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
573                raise PDT_SelectionError
574        else:
575            pg.error = f"{PDT_ERR_SEL_2_VERTIO} {len(verts)})"
576            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
577            raise PDT_SelectionError
578    elif obj.mode == "OBJECT":
579        objs = context.view_layer.objects.selected
580        if len(objs) < 2:
581            pg.error = f"{PDT_ERR_SEL_2_OBJS} {len(objs)})"
582            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
583            raise PDT_SelectionError
584        objs_s = [ob for ob in objs if ob.name != obj.name]
585        vector_a = obj.matrix_world.decompose()[0]
586        vector_b = objs_s[-1].matrix_world.decompose()[0]
587    if plane == "LO":
588        vector_difference = vector_b - vector_a
589        vector_b = view_coords_i(vector_difference.x, vector_difference.y, vector_difference.z)
590        vector_a = Vector((0, 0, 0))
591        v0 = np.array([vector_a.x + 1, vector_a.y]) - np.array([vector_a.x, vector_a.y])
592        v1 = np.array([vector_b.x, vector_b.y]) - np.array([vector_a.x, vector_a.y])
593    else:
594        a1, a2, _ = set_mode(plane)
595        v0 = np.array([vector_a[a1] + 1, vector_a[a2]]) - np.array([vector_a[a1], vector_a[a2]])
596        v1 = np.array([vector_b[a1], vector_b[a2]]) - np.array([vector_a[a1], vector_a[a2]])
597    ang = np.rad2deg(np.arctan2(np.linalg.det([v0, v1]), np.dot(v0, v1)))
598    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
599    if flip_angle:
600        if ang > 0:
601            pg.angle = round(ang - 180, decimal_places)
602        else:
603            pg.angle = round(ang - 180, decimal_places)
604    else:
605        pg.angle = round(ang, decimal_places)
606    if plane == "LO":
607        pg.distance = round(sqrt(
608            (vector_a.x - vector_b.x) ** 2 +
609            (vector_a.y - vector_b.y) ** 2), decimal_places)
610    else:
611        pg.distance = round(sqrt(
612            (vector_a[a1] - vector_b[a1]) ** 2 +
613            (vector_a[a2] - vector_b[a2]) ** 2), decimal_places)
614    pg.cartesian_coords = Vector(([round(i, decimal_places) for i in vector_b - vector_a]))
615
616
617def set_angle_distance_three(context):
618    """Measures Angle and Offsets between 3 Points in World Space, Also sets Deltas.
619
620    Note:
621        Uses 3 Selected Vertices to set pg.angle and pg.distance scene variables
622        also sets delta offset from these 3 points using standard Numpy Routines
623        Works in Edit and Oject Modes.
624
625    Args:
626        context: Blender bpy.context instance.
627
628    Returns:
629        Status Set.
630    """
631
632    pg = context.scene.pdt_pg
633    flip_angle = pg.flip_angle
634    obj = context.view_layer.objects.active
635    if obj is None:
636        pg.error = PDT_ERR_NO_ACT_OBJ
637        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
638        raise PDT_NoObjectError
639    if obj.mode == "EDIT":
640        bm = bmesh.from_edit_mesh(obj.data)
641        verts = [v for v in bm.verts if v.select]
642        if len(verts) == 3:
643            if len(bm.select_history) == 3:
644                vector_a, vector_b, vector_c = check_selection(3, bm, obj)
645                if vector_a is None:
646                    pg.error = PDT_ERR_VERT_MODE
647                    context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
648                    raise PDT_FeatureError
649            else:
650                pg.error = f"{PDT_ERR_SEL_3_VERTIO} {len(bm.select_history)})"
651                context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
652                raise PDT_SelectionError
653        else:
654            pg.error = f"{PDT_ERR_SEL_3_VERTIO} {len(verts)})"
655            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
656            raise PDT_SelectionError
657    elif obj.mode == "OBJECT":
658        objs = context.view_layer.objects.selected
659        if len(objs) < 3:
660            pg.error = PDT_ERR_SEL_3_OBJS + str(len(objs))
661            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
662            raise PDT_SelectionError
663        objs_s = [ob for ob in objs if ob.name != obj.name]
664        vector_a = obj.matrix_world.decompose()[0]
665        vector_b = objs_s[-1].matrix_world.decompose()[0]
666        vector_c = objs_s[-2].matrix_world.decompose()[0]
667    ba = np.array([vector_b.x, vector_b.y, vector_b.z]) - np.array(
668        [vector_a.x, vector_a.y, vector_a.z]
669    )
670    bc = np.array([vector_c.x, vector_c.y, vector_c.z]) - np.array(
671        [vector_a.x, vector_a.y, vector_a.z]
672    )
673    angle_cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
674    ang = np.degrees(np.arccos(angle_cosine))
675    decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
676    if flip_angle:
677        if ang > 0:
678            pg.angle = round(ang - 180, decimal_places)
679        else:
680            pg.angle = round(ang - 180, decimal_places)
681    else:
682        pg.angle = round(ang, decimal_places)
683    pg.distance = round((vector_a - vector_b).length, decimal_places)
684    pg.cartesian_coords = Vector(([round(i, decimal_places) for i in vector_b - vector_a]))
685
686
687def origin_to_cursor(context):
688    """Sets Object Origin in Edit Mode to Cursor Location.
689
690    Note:
691        Keeps geometry static in World Space whilst moving Object Origin
692        Requires cursor location
693        Works in Edit and Object Modes.
694
695    Args:
696        context: Blender bpy.context instance.
697
698    Returns:
699        Status Set.
700    """
701
702    scene = context.scene
703    pg = context.scene.pdt_pg
704    obj = context.view_layer.objects.active
705    if obj is None:
706        pg.error = PDT_ERR_NO_ACT_OBJ
707        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
708        return
709    obj_loc = obj.matrix_world.decompose()[0]
710    cur_loc = scene.cursor.location
711    diff_v = obj_loc - cur_loc
712    if obj.mode == "EDIT":
713        bm = bmesh.from_edit_mesh(obj.data)
714        for v in bm.verts:
715            v.co = v.co + diff_v
716        obj.location = cur_loc
717        bmesh.update_edit_mesh(obj.data)
718        bm.select_history.clear()
719    elif obj.mode == "OBJECT":
720        for v in obj.data.vertices:
721            v.co = v.co + diff_v
722        obj.location = cur_loc
723    else:
724        pg.error = f"{PDT_ERR_EDOB_MODE} {obj.mode})"
725        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
726        raise PDT_ObjectModeError
727
728
729def taper(context):
730    """Taper Geometry along World Axes.
731
732    Note:
733        Similar to Shear command except that it shears by angle rather than displacement.
734        Rotates about World Axes and displaces along World Axes, angle must not exceed +-80 degrees.
735        Rotation axis is centred on Active Vertex.
736        Works only in Edit mode.
737
738    Args:
739        context: Blender bpy.context instance.
740
741    Note:
742        Uses pg.taper & pg.angle scene variables
743
744    Returns:
745        Status Set.
746    """
747
748    scene = context.scene
749    pg = scene.pdt_pg
750    tap_ax = pg.taper
751    ang_v = pg.angle
752    obj = context.view_layer.objects.active
753    if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
754        if ang_v > 80 or ang_v < -80:
755            pg.error = f"{PDT_ERR_TAPER_ANG} {ang_v})"
756            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
757            raise PDT_InvalidAngle
758        if obj is None:
759            pg.error = PDT_ERR_NO_ACT_OBJ
760            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
761            raise PDT_NoObjectError
762        _, a2, a3 = set_axis(tap_ax)
763        bm = bmesh.from_edit_mesh(obj.data)
764        if len(bm.select_history) >= 1:
765            rotate_vertex = bm.select_history[-1]
766            view_vector = view_coords(rotate_vertex.co.x, rotate_vertex.co.y, rotate_vertex.co.z)
767        else:
768            pg.error = f"{PDT_ERR_TAPER_SEL} {len(bm.select_history)})"
769            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
770            raise PDT_SelectionError
771        for v in [v for v in bm.verts if v.select]:
772            if pg.plane == "LO":
773                v_loc = view_coords(v.co.x, v.co.y, v.co.z)
774                dis_v = sqrt((view_vector.x - v_loc.x) ** 2 + (view_vector.y - v_loc.y) ** 2)
775                x_loc = dis_v * tan(ang_v * pi / 180)
776                view_matrix = view_dir(x_loc, 0)
777                v.co = v.co - view_matrix
778            else:
779                dis_v = sqrt(
780                    (rotate_vertex.co[a3] - v.co[a3]) ** 2 + (rotate_vertex.co[a2] - v.co[a2]) ** 2
781                )
782                v.co[a2] = v.co[a2] - (dis_v * tan(ang_v * pi / 180))
783        bmesh.update_edit_mesh(obj.data)
784        bm.select_history.clear()
785    else:
786        pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
787        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
788        raise PDT_ObjectModeError
789