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#
24from bpy.types import Operator
25from .pdt_msg_strings import (
26    PDT_ERR_NON_VALID,
27    PDT_LAB_ABS,
28    PDT_LAB_DEL,
29    PDT_LAB_DIR,
30    PDT_LAB_INTERSECT,
31    PDT_LAB_PERCENT,
32)
33
34
35class PDT_OT_PlacementAbs(Operator):
36    """Use Absolute, or Global Placement."""
37
38    bl_idname = "pdt.absolute"
39    bl_label = "Absolute Mode"
40    bl_options = {"REGISTER", "UNDO"}
41
42    def execute(self, context):
43        """Manipulates Geometry, or Objects by Absolute (World) Coordinates.
44
45        Note:
46            - Reads pg.operate from Operation Mode Selector as 'operation'
47            - Reads pg.cartesian_coords scene variables to:
48            -- set position of CUrsor      (CU)
49            -- set postion of Pivot Point  (PP)
50            -- MoVe geometry/objects       (MV)
51            -- Extrude Vertices            (EV)
52            -- Split Edges                 (SE)
53            -- add a New Vertex            (NV)
54
55            Invalid Options result in self.report Error.
56
57        Args:
58            context: Blender bpy.context instance.
59
60        Returns:
61            Status Set.
62        """
63
64        pg = context.scene.pdt_pg
65        operation = pg.operation
66        decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
67
68        if operation == "CU":
69            # Cursor
70            pg.command = (
71                f"ca{str(round(pg.cartesian_coords.x, decimal_places))}"
72                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
73                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
74            )
75        elif operation == "PP":
76            # Pivot Point
77            pg.command = (
78                f"pa{str(round(pg.cartesian_coords.x, decimal_places))}"
79                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
80                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
81            )
82        elif operation == "MV":
83            # Move Entities
84            pg.command = (
85                f"ga{str(round(pg.cartesian_coords.x, decimal_places))}"
86                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
87                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
88            )
89        elif operation == "SE":
90            # Split Edges
91            pg.command = (
92                f"sa{str(round(pg.cartesian_coords.x, decimal_places))}"
93                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
94                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
95            )
96        elif operation == "NV":
97            # New Vertex
98            pg.command = (
99                f"na{str(round(pg.cartesian_coords.x, decimal_places))}"
100                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
101                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
102            )
103        elif operation == "EV":
104            # Extrude Vertices
105            pg.command = (
106                f"va{str(round(pg.cartesian_coords.x, decimal_places))}"
107                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
108                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
109            )
110        else:
111            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_ABS}"
112            self.report({"ERROR"}, error_message)
113        return {"FINISHED"}
114
115
116class PDT_OT_PlacementDelta(Operator):
117    """Use Delta, or Incremental Placement."""
118
119    bl_idname = "pdt.delta"
120    bl_label = "Delta Mode"
121    bl_options = {"REGISTER", "UNDO"}
122
123    def execute(self, context):
124        """Manipulates Geometry, or Objects by Delta Offset (Increment).
125
126        Note:
127            - Reads pg.operation from Operation Mode Selector as 'operation'
128            - Reads pg.select, pg.plane, pg.cartesian_coords scene variables to:
129            -- set position of CUrsor       (CU)
130            -- set position of Pivot Point  (PP)
131            -- MoVe geometry/objects        (MV)
132            -- Extrude Vertices             (EV)
133            -- Split Edges                  (SE)
134            -- add a New Vertex             (NV)
135            -- Duplicate Geometry           (DG)
136            -- Extrude Geometry             (EG)
137
138            Invalid Options result in self.report Error.
139
140        Args:
141            context: Blender bpy.context instance.
142
143        Returns:
144            Status Set.
145        """
146
147        pg = context.scene.pdt_pg
148        operation = pg.operation
149        decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
150
151        if operation == "CU":
152            # Cursor
153            pg.command = (
154                f"cd{str(round(pg.cartesian_coords.x, decimal_places))}"
155                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
156                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
157            )
158        elif operation == "PP":
159            # Pivot Point
160            pg.command = (
161                f"pd{str(round(pg.cartesian_coords.x, decimal_places))}"
162                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
163                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
164            )
165        elif operation == "MV":
166            # Move Entities
167            pg.command = (
168                f"gd{str(round(pg.cartesian_coords.x, decimal_places))}"
169                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
170                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
171            )
172        elif operation == "SE":
173            # Split Edges
174            pg.command = (
175                f"sd{str(round(pg.cartesian_coords.x, decimal_places))}"
176                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
177                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
178            )
179        elif operation == "NV":
180            # New Vertex
181            pg.command = (
182                f"nd{str(round(pg.cartesian_coords.x, decimal_places))}"
183                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
184                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
185            )
186        elif operation == "EV":
187            # Extrue Vertices
188            pg.command = (
189                f"vd{str(round(pg.cartesian_coords.x, decimal_places))}"
190                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
191                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
192            )
193        elif operation == "DG":
194            # Duplicate Entities
195            pg.command = (
196                f"dd{str(round(pg.cartesian_coords.x, decimal_places))}"
197                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
198                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
199            )
200        elif operation == "EG":
201            # Extrue Geometry
202            pg.command = (
203                f"ed{str(round(pg.cartesian_coords.x, decimal_places))}"
204                f",{str(round(pg.cartesian_coords.y, decimal_places))}"
205                f",{str(round(pg.cartesian_coords.z, decimal_places))}"
206            )
207        else:
208            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_DEL}"
209            self.report({"ERROR"}, error_message)
210        return {"FINISHED"}
211
212
213class PDT_OT_PlacementDis(Operator):
214    """Use Directional, or Distance @ Angle Placement."""
215
216    bl_idname = "pdt.distance"
217    bl_label = "Distance@Angle Mode"
218    bl_options = {"REGISTER", "UNDO"}
219
220    def execute(self, context):
221        """Manipulates Geometry, or Objects by Distance at Angle (Direction).
222
223        Note:
224            - Reads pg.operation from Operation Mode Selector as 'operation'
225            - Reads pg.select, pg.distance, pg.angle, pg.plane & pg.flip_angle scene variables to:
226            -- set position of CUrsor       (CU)
227            -- set position of Pivot Point  (PP)
228            -- MoVe geometry/objects        (MV)
229            -- Extrude Vertices             (EV)
230            -- Split Edges                  (SE)
231            -- add a New Vertex             (NV)
232            -- Duplicate Geometry           (DG)
233            -- Extrude Geometry             (EG)
234
235            Invalid Options result in self.report Error.
236
237        Args:
238            context: Blender bpy.context instance.
239
240        Returns:
241            Status Set.
242        """
243
244        pg = context.scene.pdt_pg
245        operation = pg.operation
246        decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
247
248        if operation == "CU":
249            # Cursor
250            pg.command = (
251                f"ci{str(round(pg.distance, decimal_places))}"
252                f",{str(round(pg.angle, decimal_places))}"
253            )
254        elif operation == "PP":
255            # Pivot Point
256            pg.command = (
257                f"pi{str(round(pg.distance, decimal_places))}"
258                f",{str(round(pg.angle, decimal_places))}"
259            )
260        elif operation == "MV":
261            # Move Entities
262            pg.command = (
263                f"gi{str(round(pg.distance, decimal_places))}"
264                f",{str(round(pg.angle, decimal_places))}"
265            )
266        elif operation == "SE":
267            # Split Edges
268            pg.command = (
269                f"si{str(round(pg.distance, decimal_places))}"
270                f",{str(round(pg.angle, decimal_places))}"
271                )
272        elif operation == "NV":
273            # New Vertex
274            pg.command = (
275                f"ni{str(round(pg.distance, decimal_places))}"
276                f",{str(round(pg.angle, decimal_places))}"
277            )
278        elif operation == "EV":
279            # Extrude Vertices
280            pg.command = (
281                f"vi{str(round(pg.distance, decimal_places))}"
282                f",{str(round(pg.angle, decimal_places))}"
283            )
284        elif operation == "DG":
285            # Duplicate Geometry
286            pg.command = (
287                f"di{str(round(pg.distance, decimal_places))}"
288                f",{str(round(pg.angle, decimal_places))}"
289            )
290        elif operation == "EG":
291            # Extrude Geometry
292            pg.command = (
293                f"ei{str(round(pg.distance, decimal_places))}"
294                f",{str(round(pg.angle, decimal_places))}"
295            )
296        else:
297            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_DIR}"
298            self.report({"ERROR"}, error_message)
299        return {"FINISHED"}
300
301
302class PDT_OT_PlacementPer(Operator):
303    """Use Percentage Placement."""
304
305    bl_idname = "pdt.percent"
306    bl_label = "Percentage Mode"
307    bl_options = {"REGISTER", "UNDO"}
308
309    def execute(self, context):
310        """Manipulates Geometry, or Objects by Percentage between 2 points.
311
312        Note:
313            - Reads pg.operation from Operation Mode Selector as 'operation'
314            - Reads pg.percent, pg.extend & pg.flip_percent scene variables to:
315            -- set position of CUrsor       (CU)
316            -- set position of Pivot Point  (PP)
317            -- MoVe geometry/objects        (MV)
318            -- Extrude Vertices             (EV)
319            -- Split Edges                  (SE)
320            -- add a New Vertex             (NV)
321
322            Invalid Options result in self.report Error.
323
324        Args:
325            context: Blender bpy.context instance.
326
327        Returns:
328            Status Set.
329        """
330
331        pg = context.scene.pdt_pg
332        operation = pg.operation
333        decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
334
335        if operation == "CU":
336            # Cursor
337            pg.command = f"cp{str(round(pg.percent, decimal_places))}"
338        elif operation == "PP":
339            # Pivot Point
340            pg.command = f"pp{str(round(pg.percent, decimal_places))}"
341        elif operation == "MV":
342            # Move Entities
343            pg.command = f"gp{str(round(pg.percent, decimal_places))}"
344        elif operation == "SE":
345            # Split Edges
346            pg.command = f"sp{str(round(pg.percent, decimal_places))}"
347        elif operation == "NV":
348            # New Vertex
349            pg.command = f"np{str(round(pg.percent, decimal_places))}"
350        elif operation == "EV":
351            # Extrude Vertices
352            pg.command = f"vp{str(round(pg.percent, decimal_places))}"
353        else:
354            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_PERCENT}"
355            self.report({"ERROR"}, error_message)
356        return {"FINISHED"}
357
358
359class PDT_OT_PlacementNormal(Operator):
360    """Use Normal, or Perpendicular Placement."""
361
362    bl_idname = "pdt.normal"
363    bl_label = "Normal Mode"
364    bl_options = {"REGISTER", "UNDO"}
365
366    def execute(self, context):
367        """Manipulates Geometry, or Objects by Normal Intersection between 3 points.
368
369        Note:
370            - Reads pg.operation from Operation Mode Selector as 'operation'
371            - Reads pg.extend scene variable to:
372            -- set position of CUrsor       (CU)
373            -- set position of Pivot Point  (PP)
374            -- MoVe geometry/objects        (MV)
375            -- Extrude Vertices             (EV)
376            -- Split Edges                  (SE)
377            -- add a New Vertex             (NV)
378
379            Invalid Options result in self.report Error.
380
381        Args:
382            context: Blender bpy.context instance.
383
384        Returns:
385            Status Set.
386        """
387
388        pg = context.scene.pdt_pg
389        operation = pg.operation
390        if operation == "CU":
391            pg.command = f"cnml"
392        elif operation == "PP":
393            pg.command = f"pnml"
394        elif operation == "MV":
395            pg.command = f"gnml"
396        elif operation == "EV":
397            pg.command = f"vnml"
398        elif operation == "SE":
399            pg.command = f"snml"
400        elif operation == "NV":
401            pg.command = f"nnml"
402        else:
403            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}"
404            self.report({"ERROR"}, error_message)
405        return {"FINISHED"}
406
407
408class PDT_OT_PlacementCen(Operator):
409    """Use Placement at Arc Centre."""
410
411    bl_idname = "pdt.centre"
412    bl_label = "Centre Mode"
413    bl_options = {"REGISTER", "UNDO"}
414
415    def execute(self, context):
416        """Manipulates Geometry, or Objects to an Arc Centre defined by 3 points on an Imaginary Arc.
417
418        Note:
419            - Reads pg.operation from Operation Mode Selector as 'operation'
420            -- set position of CUrsor       (CU)
421            -- set position of Pivot Point  (PP)
422            -- MoVe geometry/objects        (MV)
423            -- Extrude Vertices             (EV)
424            -- add a New vertex             (NV)
425
426            Invalid Options result in self.report Error.
427
428        Args:
429            context: Blender bpy.context instance.
430
431        Returns:
432            Status Set.
433        """
434
435        pg = context.scene.pdt_pg
436        operation = pg.operation
437        if operation == "CU":
438            pg.command = f"ccen"
439        elif operation == "PP":
440            pg.command = f"pcen"
441        elif operation == "MV":
442            pg.command = f"gcen"
443        elif operation == "EV":
444            pg.command = f"vcen"
445        elif operation == "NV":
446            pg.command = f"ncen"
447        else:
448            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}"
449            self.report({"ERROR"}, error_message)
450        return {"FINISHED"}
451
452
453class PDT_OT_PlacementInt(Operator):
454    """Use Intersection, or Convergence Placement."""
455
456    bl_idname = "pdt.intersect"
457    bl_label = "Intersect Mode"
458    bl_options = {"REGISTER", "UNDO"}
459
460    def execute(self, context):
461        """Manipulates Geometry, or Objects by Convergance Intersection between 4 points, or 2 Edges.
462
463        Note:
464            - Reads pg.operation from Operation Mode Selector as 'operation'
465            - Reads pg.plane scene variable and operates in Working Plane to:
466            -- set position of CUrsor       (CU)
467            -- set position of Pivot Point  (PP)
468            -- MoVe geometry/objects        (MV)
469            -- Extrude Vertices             (EV)
470            -- add a New vertex             (NV)
471
472            Invalid Options result in "self.report" Error.
473
474        Args:
475            context: Blender bpy.context instance.
476
477        Returns:
478            Status Set.
479        """
480
481        pg = context.scene.pdt_pg
482        operation = pg.operation
483        if operation == "CU":
484            pg.command = f"cint"
485        elif operation == "PP":
486            pg.command = f"pint"
487        elif operation == "MV":
488            pg.command = f"gint"
489        elif operation == "EV":
490            pg.command = f"vint"
491        elif operation == "NV":
492            pg.command = f"nint"
493        else:
494            error_message = f"{operation} {PDT_ERR_NON_VALID} {PDT_LAB_INTERSECT}"
495            self.report({"ERROR"}, error_message)
496        return {"FINISHED"}
497
498
499class PDT_OT_JoinVerts(Operator):
500    """Join 2 Free Vertices into an Edge."""
501
502    bl_idname = "pdt.join"
503    bl_label = "Join 2 Vertices"
504    bl_options = {"REGISTER", "UNDO"}
505
506    @classmethod
507    def poll(cls, context):
508        ob = context.object
509        if ob is None:
510            return False
511        return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
512
513    def execute(self, context):
514        """Joins 2 Free Vertices that do not form part of a Face.
515
516        Note:
517            Joins two vertices that do not form part of a single face
518            It is designed to close open Edge Loops, where a face is not required
519            or to join two disconnected Edges.
520
521        Args:
522            context: Blender bpy.context instance.
523
524        Returns:
525            Status Set.
526        """
527
528        pg = context.scene.pdt_pg
529        pg.command = f"j2v"
530        return {"FINISHED"}
531
532
533class PDT_OT_Fillet(Operator):
534    """Fillet Edges by Vertex, Set Use Verts to False for Extruded Structure."""
535
536    bl_idname = "pdt.fillet"
537    bl_label = "Fillet"
538    bl_options = {"REGISTER", "UNDO"}
539
540    @classmethod
541    def poll(cls, context):
542        ob = context.object
543        if ob is None:
544            return False
545        return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
546
547    def execute(self, context):
548        """Create Fillets by Vertex or by Geometry.
549
550        Note:
551            Fillets connected edges, or connected faces
552        Uses:
553            - pg.fillet_radius  ; Radius of fillet
554            - pg.fillet_segments  ; Number of segments
555            - pg.fillet_profile  ; Profile, values 0 to 1
556            - pg.fillet_vertices_only ; Vertices (True), or Face/Edges
557            - pg.fillet_intersect ; Intersect dges first (True), or not
558
559        Args:
560            context: Blender bpy.context instance.
561
562        Returns:
563            Status Set.
564        """
565
566        pg = context.scene.pdt_pg
567        decimal_places = context.preferences.addons[__package__].preferences.pdt_input_round
568        if pg.fillet_intersect:
569            pg.command = (
570                f"fi{str(round(pg.fillet_radius, decimal_places))}"
571                f",{str(round(pg.fillet_segments, decimal_places))}"
572                f",{str(round(pg.fillet_profile, decimal_places))}"
573            )
574        elif pg.fillet_vertices_only:
575            pg.command = (
576                f"fv{str(round(pg.fillet_radius, decimal_places))}"
577                f",{str(round(pg.fillet_segments, decimal_places))}"
578                f",{str(round(pg.fillet_profile, decimal_places))}"
579            )
580        else:
581            pg.command = (
582                f"fe{str(round(pg.fillet_radius, decimal_places))}"
583                f",{str(round(pg.fillet_segments, decimal_places))}"
584                f",{str(round(pg.fillet_profile, decimal_places))}"
585            )
586        return {"FINISHED"}
587
588
589class PDT_OT_Angle2(Operator):
590    """Measure Distance and Angle in Working Plane, Also sets Deltas."""
591
592    bl_idname = "pdt.angle2"
593    bl_label = "Measure 2D"
594    bl_options = {"REGISTER", "UNDO"}
595
596    def execute(self, context):
597        """Measures Angle and Offsets between 2 Points in View Plane.
598
599        Note:
600            Uses 2 Selected Vertices to set pg.angle and pg.distance scene variables
601            also sets delta offset from these 2 points using standard Numpy Routines
602            Works in Edit and Oject Modes.
603
604        Args:
605            context: Blender bpy.context instance.
606
607        Returns:
608            Status Set.
609        """
610
611        pg = context.scene.pdt_pg
612        pg.command = f"ad2"
613        return {"FINISHED"}
614
615
616class PDT_OT_Angle3(Operator):
617    """Measure Distance and Angle in 3D Space."""
618
619    bl_idname = "pdt.angle3"
620    bl_label = "Measure 3D"
621    bl_options = {"REGISTER", "UNDO"}
622
623    def execute(self, context):
624        """Measures Angle and Offsets between 3 Points in World Space, Also sets Deltas.
625
626        Note:
627            Uses 3 Selected Vertices to set pg.angle and pg.distance scene variables
628            also sets delta offset from these 3 points using standard Numpy Routines
629            Works in Edit and Oject Modes.
630
631        Args:
632            context: Blender bpy.context instance.
633
634        Returns:
635            Status Set.
636        """
637
638        pg = context.scene.pdt_pg
639        pg.command = f"ad3"
640        return {"FINISHED"}
641
642
643class PDT_OT_Origin(Operator):
644    """Move Object Origin to Cursor Location."""
645
646    bl_idname = "pdt.origin"
647    bl_label = "Move Origin"
648    bl_options = {"REGISTER", "UNDO"}
649
650    def execute(self, context):
651        """Sets Object Origin in Edit Mode to Cursor Location.
652
653        Note:
654            Keeps geometry static in World Space whilst moving Object Origin
655            Requires cursor location
656            Works in Edit and Object Modes.
657
658        Args:
659            context: Blender bpy.context instance.
660
661        Returns:
662            Status Set.
663        """
664
665        pg = context.scene.pdt_pg
666        pg.command = f"otc"
667        return {"FINISHED"}
668
669
670class PDT_OT_Taper(Operator):
671    """Taper Vertices at Angle in Chosen Axis Mode."""
672
673    bl_idname = "pdt.taper"
674    bl_label = "Taper"
675    bl_options = {"REGISTER", "UNDO"}
676
677    @classmethod
678    def poll(cls, context):
679        ob = context.object
680        if ob is None:
681            return False
682        return all([bool(ob), ob.type == "MESH", ob.mode == "EDIT"])
683
684    def execute(self, context):
685        """Taper Geometry along World Axes.
686
687        Note:
688            Similar to Blender Shear command except that it shears by angle rather than displacement.
689            Rotates about World Axes and displaces along World Axes, angle must not exceed +-80 degrees.
690            Rotation axis is centred on Active Vertex.
691            Works only in Edit mode.
692
693        Args:
694            context: Blender bpy.context instance.
695
696        Note:
697            Uses pg.taper & pg.angle scene variables
698
699        Returns:
700            Status Set.
701        """
702
703        pg = context.scene.pdt_pg
704        pg.command = f"tap"
705        return {"FINISHED"}
706