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