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