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