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 20bl_info = { 21 "name": "Offset Edges", 22 "author": "Hidesato Ikeya, Veezen fix 2.8 (temporary)", 23 #i tried edit newest version, but got some errors, works only on 0,2,6 24 "version": (0, 2, 6), 25 "blender": (2, 80, 0), 26 "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges", 27 "description": "Offset Edges", 28 "warning": "", 29 "doc_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges", 30 "tracker_url": "", 31 "category": "Mesh", 32} 33 34import math 35from math import sin, cos, pi, copysign, radians 36import bpy 37from bpy_extras import view3d_utils 38import bmesh 39from mathutils import Vector 40from time import perf_counter 41 42X_UP = Vector((1.0, .0, .0)) 43Y_UP = Vector((.0, 1.0, .0)) 44Z_UP = Vector((.0, .0, 1.0)) 45ZERO_VEC = Vector((.0, .0, .0)) 46ANGLE_90 = pi / 2 47ANGLE_180 = pi 48ANGLE_360 = 2 * pi 49 50 51def calc_loop_normal(verts, fallback=Z_UP): 52 # Calculate normal from verts using Newell's method. 53 normal = ZERO_VEC.copy() 54 55 if verts[0] is verts[-1]: 56 # Perfect loop 57 range_verts = range(1, len(verts)) 58 else: 59 # Half loop 60 range_verts = range(0, len(verts)) 61 62 for i in range_verts: 63 v1co, v2co = verts[i-1].co, verts[i].co 64 normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z) 65 normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x) 66 normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y) 67 68 if normal != ZERO_VEC: 69 normal.normalize() 70 else: 71 normal = fallback 72 73 return normal 74 75def collect_edges(bm): 76 set_edges_orig = set() 77 for e in bm.edges: 78 if e.select: 79 co_faces_selected = 0 80 for f in e.link_faces: 81 if f.select: 82 co_faces_selected += 1 83 if co_faces_selected == 2: 84 break 85 else: 86 set_edges_orig.add(e) 87 88 if not set_edges_orig: 89 return None 90 91 return set_edges_orig 92 93def collect_loops(set_edges_orig): 94 set_edges_copy = set_edges_orig.copy() 95 96 loops = [] # [v, e, v, e, ... , e, v] 97 while set_edges_copy: 98 edge_start = set_edges_copy.pop() 99 v_left, v_right = edge_start.verts 100 lp = [v_left, edge_start, v_right] 101 reverse = False 102 while True: 103 edge = None 104 for e in v_right.link_edges: 105 if e in set_edges_copy: 106 if edge: 107 # Overlap detected. 108 return None 109 edge = e 110 set_edges_copy.remove(e) 111 if edge: 112 v_right = edge.other_vert(v_right) 113 lp.extend((edge, v_right)) 114 continue 115 else: 116 if v_right is v_left: 117 # Real loop. 118 loops.append(lp) 119 break 120 elif reverse is False: 121 # Right side of half loop. 122 # Reversing the loop to operate same procedure on the left side. 123 lp.reverse() 124 v_right, v_left = v_left, v_right 125 reverse = True 126 continue 127 else: 128 # Half loop, completed. 129 loops.append(lp) 130 break 131 return loops 132 133def get_adj_ix(ix_start, vec_edges, half_loop): 134 # Get adjacent edge index, skipping zero length edges 135 len_edges = len(vec_edges) 136 if half_loop: 137 range_right = range(ix_start, len_edges) 138 range_left = range(ix_start-1, -1, -1) 139 else: 140 range_right = range(ix_start, ix_start+len_edges) 141 range_left = range(ix_start-1, ix_start-1-len_edges, -1) 142 143 ix_right = ix_left = None 144 for i in range_right: 145 # Right 146 i %= len_edges 147 if vec_edges[i] != ZERO_VEC: 148 ix_right = i 149 break 150 for i in range_left: 151 # Left 152 i %= len_edges 153 if vec_edges[i] != ZERO_VEC: 154 ix_left = i 155 break 156 if half_loop: 157 # If index of one side is None, assign another index. 158 if ix_right is None: 159 ix_right = ix_left 160 if ix_left is None: 161 ix_left = ix_right 162 163 return ix_right, ix_left 164 165def get_adj_faces(edges): 166 adj_faces = [] 167 for e in edges: 168 adj_f = None 169 co_adj = 0 170 for f in e.link_faces: 171 # Search an adjacent face. 172 # Selected face has precedance. 173 if not f.hide and f.normal != ZERO_VEC: 174 adj_exist = True 175 adj_f = f 176 co_adj += 1 177 if f.select: 178 adj_faces.append(adj_f) 179 break 180 else: 181 if co_adj == 1: 182 adj_faces.append(adj_f) 183 else: 184 adj_faces.append(None) 185 return adj_faces 186 187 188def get_edge_rail(vert, set_edges_orig): 189 co_edges = co_edges_selected = 0 190 vec_inner = None 191 for e in vert.link_edges: 192 if (e not in set_edges_orig and 193 (e.select or (co_edges_selected == 0 and not e.hide))): 194 v_other = e.other_vert(vert) 195 vec = v_other.co - vert.co 196 if vec != ZERO_VEC: 197 vec_inner = vec 198 if e.select: 199 co_edges_selected += 1 200 if co_edges_selected == 2: 201 return None 202 else: 203 co_edges += 1 204 if co_edges_selected == 1: 205 vec_inner.normalize() 206 return vec_inner 207 elif co_edges == 1: 208 # No selected edges, one unselected edge. 209 vec_inner.normalize() 210 return vec_inner 211 else: 212 return None 213 214def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l): 215 # Cross rail is a cross vector between normal_r and normal_l. 216 217 vec_cross = normal_r.cross(normal_l) 218 if vec_cross.dot(vec_tan) < .0: 219 vec_cross *= -1 220 cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l)) 221 cos = vec_tan.dot(vec_cross) 222 if cos >= cos_min: 223 vec_cross.normalize() 224 return vec_cross 225 else: 226 return None 227 228def move_verts(width, depth, verts, directions, geom_ex): 229 if geom_ex: 230 geom_s = geom_ex['side'] 231 verts_ex = [] 232 for v in verts: 233 for e in v.link_edges: 234 if e in geom_s: 235 verts_ex.append(e.other_vert(v)) 236 break 237 #assert len(verts) == len(verts_ex) 238 verts = verts_ex 239 240 for v, (vec_width, vec_depth) in zip(verts, directions): 241 v.co += width * vec_width + depth * vec_depth 242 243def extrude_edges(bm, edges_orig): 244 extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom'] 245 n_edges = n_faces = len(edges_orig) 246 n_verts = len(extruded) - n_edges - n_faces 247 248 geom = dict() 249 geom['verts'] = verts = set(extruded[:n_verts]) 250 geom['edges'] = edges = set(extruded[n_verts:n_verts + n_edges]) 251 geom['faces'] = set(extruded[n_verts + n_edges:]) 252 geom['side'] = set(e for v in verts for e in v.link_edges if e not in edges) 253 254 return geom 255 256def clean(bm, mode, edges_orig, geom_ex=None): 257 for f in bm.faces: 258 f.select = False 259 if geom_ex: 260 for e in geom_ex['edges']: 261 e.select = True 262 if mode == 'offset': 263 lis_geom = list(geom_ex['side']) + list(geom_ex['faces']) 264 bmesh.ops.delete(bm, geom=lis_geom, context='EDGES') 265 else: 266 for e in edges_orig: 267 e.select = True 268 269def collect_mirror_planes(edit_object): 270 mirror_planes = [] 271 eob_mat_inv = edit_object.matrix_world.inverted() 272 273 274 for m in edit_object.modifiers: 275 if (m.type == 'MIRROR' and m.use_mirror_merge): 276 merge_limit = m.merge_threshold 277 if not m.mirror_object: 278 loc = ZERO_VEC 279 norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP 280 else: 281 mirror_mat_local = eob_mat_inv @ m.mirror_object.matrix_world 282 loc = mirror_mat_local.to_translation() 283 norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated() 284 norm_x = norm_x.to_3d().normalized() 285 norm_y = norm_y.to_3d().normalized() 286 norm_z = norm_z.to_3d().normalized() 287 if m.use_axis[0]: 288 mirror_planes.append((loc, norm_x, merge_limit)) 289 if m.use_axis[1]: 290 mirror_planes.append((loc, norm_y, merge_limit)) 291 if m.use_axis[2]: 292 mirror_planes.append((loc, norm_z, merge_limit)) 293 return mirror_planes 294 295def get_vert_mirror_pairs(set_edges_orig, mirror_planes): 296 if mirror_planes: 297 set_edges_copy = set_edges_orig.copy() 298 vert_mirror_pairs = dict() 299 for e in set_edges_orig: 300 v1, v2 = e.verts 301 for mp in mirror_planes: 302 p_co, p_norm, mlimit = mp 303 v1_dist = abs(p_norm.dot(v1.co - p_co)) 304 v2_dist = abs(p_norm.dot(v2.co - p_co)) 305 if v1_dist <= mlimit: 306 # v1 is on a mirror plane. 307 vert_mirror_pairs[v1] = mp 308 if v2_dist <= mlimit: 309 # v2 is on a mirror plane. 310 vert_mirror_pairs[v2] = mp 311 if v1_dist <= mlimit and v2_dist <= mlimit: 312 # This edge is on a mirror_plane, so should not be offsetted. 313 set_edges_copy.remove(e) 314 return vert_mirror_pairs, set_edges_copy 315 else: 316 return None, set_edges_orig 317 318def get_mirror_rail(mirror_plane, vec_up): 319 p_norm = mirror_plane[1] 320 mirror_rail = vec_up.cross(p_norm) 321 if mirror_rail != ZERO_VEC: 322 mirror_rail.normalize() 323 # Project vec_up to mirror_plane 324 vec_up = vec_up - vec_up.project(p_norm) 325 vec_up.normalize() 326 return mirror_rail, vec_up 327 else: 328 return None, vec_up 329 330def reorder_loop(verts, edges, lp_normal, adj_faces): 331 for i, adj_f in enumerate(adj_faces): 332 if adj_f is None: 333 continue 334 v1, v2 = verts[i], verts[i+1] 335 e = edges[i] 336 fv = tuple(adj_f.verts) 337 if fv[fv.index(v1)-1] is v2: 338 # Align loop direction 339 verts.reverse() 340 edges.reverse() 341 adj_faces.reverse() 342 if lp_normal.dot(adj_f.normal) < .0: 343 lp_normal *= -1 344 break 345 else: 346 # All elements in adj_faces are None 347 for v in verts: 348 if v.normal != ZERO_VEC: 349 if lp_normal.dot(v.normal) < .0: 350 verts.reverse() 351 edges.reverse() 352 lp_normal *= -1 353 break 354 355 return verts, edges, lp_normal, adj_faces 356 357def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options): 358 opt_follow_face = options['follow_face'] 359 opt_edge_rail = options['edge_rail'] 360 opt_er_only_end = options['edge_rail_only_end'] 361 opt_threshold = options['threshold'] 362 363 verts, edges = lp[::2], lp[1::2] 364 set_edges = set(edges) 365 lp_normal = calc_loop_normal(verts, fallback=normal_fallback) 366 367 ##### Loop order might be changed below. 368 if lp_normal.dot(vec_upward) < .0: 369 # Make this loop's normal towards vec_upward. 370 verts.reverse() 371 edges.reverse() 372 lp_normal *= -1 373 374 if opt_follow_face: 375 adj_faces = get_adj_faces(edges) 376 verts, edges, lp_normal, adj_faces = \ 377 reorder_loop(verts, edges, lp_normal, adj_faces) 378 else: 379 adj_faces = (None, ) * len(edges) 380 ##### Loop order might be changed above. 381 382 vec_edges = tuple((e.other_vert(v).co - v.co).normalized() 383 for v, e in zip(verts, edges)) 384 385 if verts[0] is verts[-1]: 386 # Real loop. Popping last vertex. 387 verts.pop() 388 HALF_LOOP = False 389 else: 390 # Half loop 391 HALF_LOOP = True 392 393 len_verts = len(verts) 394 directions = [] 395 for i in range(len_verts): 396 vert = verts[i] 397 ix_right, ix_left = i, i-1 398 399 VERT_END = False 400 if HALF_LOOP: 401 if i == 0: 402 # First vert 403 ix_left = ix_right 404 VERT_END = True 405 elif i == len_verts - 1: 406 # Last vert 407 ix_right = ix_left 408 VERT_END = True 409 410 edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left] 411 face_right, face_left = adj_faces[ix_right], adj_faces[ix_left] 412 413 norm_right = face_right.normal if face_right else lp_normal 414 norm_left = face_left.normal if face_left else lp_normal 415 if norm_right.angle(norm_left) > opt_threshold: 416 # Two faces are not flat. 417 two_normals = True 418 else: 419 two_normals = False 420 421 tan_right = edge_right.cross(norm_right).normalized() 422 tan_left = edge_left.cross(norm_left).normalized() 423 tan_avr = (tan_right + tan_left).normalized() 424 norm_avr = (norm_right + norm_left).normalized() 425 426 rail = None 427 if two_normals or opt_edge_rail: 428 # Get edge rail. 429 # edge rail is a vector of an inner edge. 430 if two_normals or (not opt_er_only_end) or VERT_END: 431 rail = get_edge_rail(vert, set_edges) 432 if vert_mirror_pairs and VERT_END: 433 if vert in vert_mirror_pairs: 434 rail, norm_avr = \ 435 get_mirror_rail(vert_mirror_pairs[vert], norm_avr) 436 if (not rail) and two_normals: 437 # Get cross rail. 438 # Cross rail is a cross vector between norm_right and norm_left. 439 rail = get_cross_rail( 440 tan_avr, edge_right, edge_left, norm_right, norm_left) 441 if rail: 442 dot = tan_avr.dot(rail) 443 if dot > .0: 444 tan_avr = rail 445 elif dot < .0: 446 tan_avr = -rail 447 448 vec_plane = norm_avr.cross(tan_avr) 449 e_dot_p_r = edge_right.dot(vec_plane) 450 e_dot_p_l = edge_left.dot(vec_plane) 451 if e_dot_p_r or e_dot_p_l: 452 if e_dot_p_r > e_dot_p_l: 453 vec_edge, e_dot_p = edge_right, e_dot_p_r 454 else: 455 vec_edge, e_dot_p = edge_left, e_dot_p_l 456 457 vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized() 458 # Make vec_tan perpendicular to vec_edge 459 vec_up = vec_tan.cross(vec_edge) 460 461 vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge 462 vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge 463 else: 464 vec_width = tan_avr 465 vec_depth = norm_avr 466 467 directions.append((vec_width, vec_depth)) 468 469 return verts, directions 470 471def use_cashes(self, context): 472 self.caches_valid = True 473 474angle_presets = {'0°': 0, 475 '15°': radians(15), 476 '30°': radians(30), 477 '45°': radians(45), 478 '60°': radians(60), 479 '75°': radians(75), 480 '90°': radians(90),} 481def assign_angle_presets(self, context): 482 use_cashes(self, context) 483 self.angle = angle_presets[self.angle_presets] 484 485class OffsetEdges(bpy.types.Operator): 486 """Offset Edges.""" 487 bl_idname = "mesh.offset_edges" 488 bl_label = "Offset Edges" 489 bl_options = {'REGISTER', 'UNDO'} 490 491 geometry_mode: bpy.props.EnumProperty( 492 items=[('offset', "Offset", "Offset edges"), 493 ('extrude', "Extrude", "Extrude edges"), 494 ('move', "Move", "Move selected edges")], 495 name="Geometory mode", default='offset', 496 update=use_cashes) 497 width: bpy.props.FloatProperty( 498 name="Width", default=.2, precision=4, step=1, update=use_cashes) 499 flip_width: bpy.props.BoolProperty( 500 name="Flip Width", default=False, 501 description="Flip width direction", update=use_cashes) 502 depth: bpy.props.FloatProperty( 503 name="Depth", default=.0, precision=4, step=1, update=use_cashes) 504 flip_depth: bpy.props.BoolProperty( 505 name="Flip Depth", default=False, 506 description="Flip depth direction", update=use_cashes) 507 depth_mode: bpy.props.EnumProperty( 508 items=[('angle', "Angle", "Angle"), 509 ('depth', "Depth", "Depth")], 510 name="Depth mode", default='angle', update=use_cashes) 511 angle: bpy.props.FloatProperty( 512 name="Angle", default=0, precision=3, step=.1, 513 min=-2*pi, max=2*pi, subtype='ANGLE', 514 description="Angle", update=use_cashes) 515 flip_angle: bpy.props.BoolProperty( 516 name="Flip Angle", default=False, 517 description="Flip Angle", update=use_cashes) 518 follow_face: bpy.props.BoolProperty( 519 name="Follow Face", default=False, 520 description="Offset along faces around") 521 mirror_modifier: bpy.props.BoolProperty( 522 name="Mirror Modifier", default=False, 523 description="Take into account of Mirror modifier") 524 edge_rail: bpy.props.BoolProperty( 525 name="Edge Rail", default=False, 526 description="Align vertices along inner edges") 527 edge_rail_only_end: bpy.props.BoolProperty( 528 name="Edge Rail Only End", default=False, 529 description="Apply edge rail to end verts only") 530 threshold: bpy.props.FloatProperty( 531 name="Flat Face Threshold", default=radians(0.05), precision=5, 532 step=1.0e-4, subtype='ANGLE', 533 description="If difference of angle between two adjacent faces is " 534 "below this value, those faces are regarded as flat.", 535 options={'HIDDEN'}) 536 caches_valid: bpy.props.BoolProperty( 537 name="Caches Valid", default=False, 538 options={'HIDDEN'}) 539 angle_presets: bpy.props.EnumProperty( 540 items=[('0°', "0°", "0°"), 541 ('15°', "15°", "15°"), 542 ('30°', "30°", "30°"), 543 ('45°', "45°", "45°"), 544 ('60°', "60°", "60°"), 545 ('75°', "75°", "75°"), 546 ('90°', "90°", "90°"), ], 547 name="Angle Presets", default='0°', 548 update=assign_angle_presets) 549 550 _cache_offset_infos = None 551 _cache_edges_orig_ixs = None 552 553 @classmethod 554 def poll(self, context): 555 return context.mode == 'EDIT_MESH' 556 557 def draw(self, context): 558 layout = self.layout 559 layout.prop(self, 'geometry_mode', text="") 560 #layout.prop(self, 'geometry_mode', expand=True) 561 562 row = layout.row(align=True) 563 row.prop(self, 'width') 564 row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True) 565 566 layout.prop(self, 'depth_mode', expand=True) 567 if self.depth_mode == 'angle': 568 d_mode = 'angle' 569 flip = 'flip_angle' 570 else: 571 d_mode = 'depth' 572 flip = 'flip_depth' 573 row = layout.row(align=True) 574 row.prop(self, d_mode) 575 row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True) 576 if self.depth_mode == 'angle': 577 layout.prop(self, 'angle_presets', text="Presets", expand=True) 578 579 layout.separator() 580 581 layout.prop(self, 'follow_face') 582 583 row = layout.row() 584 row.prop(self, 'edge_rail') 585 if self.edge_rail: 586 row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True) 587 588 layout.prop(self, 'mirror_modifier') 589 590 #layout.operator('mesh.offset_edges', text='Repeat') 591 592 if self.follow_face: 593 layout.separator() 594 layout.prop(self, 'threshold', text='Threshold') 595 596 597 def get_offset_infos(self, bm, edit_object): 598 if self.caches_valid and self._cache_offset_infos is not None: 599 # Return None, indicating to use cache. 600 return None, None 601 602 time = perf_counter() 603 604 set_edges_orig = collect_edges(bm) 605 if set_edges_orig is None: 606 self.report({'WARNING'}, 607 "No edges selected.") 608 return False, False 609 610 if self.mirror_modifier: 611 mirror_planes = collect_mirror_planes(edit_object) 612 vert_mirror_pairs, set_edges = \ 613 get_vert_mirror_pairs(set_edges_orig, mirror_planes) 614 615 if set_edges: 616 set_edges_orig = set_edges 617 else: 618 #self.report({'WARNING'}, 619 # "All selected edges are on mirror planes.") 620 vert_mirror_pairs = None 621 else: 622 vert_mirror_pairs = None 623 624 loops = collect_loops(set_edges_orig) 625 if loops is None: 626 self.report({'WARNING'}, 627 "Overlap detected. Select non-overlap edge loops") 628 return False, False 629 630 vec_upward = (X_UP + Y_UP + Z_UP).normalized() 631 # vec_upward is used to unify loop normals when follow_face is off. 632 normal_fallback = Z_UP 633 #normal_fallback = Vector(context.region_data.view_matrix[2][:3]) 634 # normal_fallback is used when loop normal cannot be calculated. 635 636 follow_face = self.follow_face 637 edge_rail = self.edge_rail 638 er_only_end = self.edge_rail_only_end 639 threshold = self.threshold 640 641 offset_infos = [] 642 for lp in loops: 643 verts, directions = get_directions( 644 lp, vec_upward, normal_fallback, vert_mirror_pairs, 645 follow_face=follow_face, edge_rail=edge_rail, 646 edge_rail_only_end=er_only_end, 647 threshold=threshold) 648 if verts: 649 offset_infos.append((verts, directions)) 650 651 # Saving caches. 652 self._cache_offset_infos = _cache_offset_infos = [] 653 for verts, directions in offset_infos: 654 v_ixs = tuple(v.index for v in verts) 655 _cache_offset_infos.append((v_ixs, directions)) 656 self._cache_edges_orig_ixs = tuple(e.index for e in set_edges_orig) 657 658 print("Preparing OffsetEdges: ", perf_counter() - time) 659 660 return offset_infos, set_edges_orig 661 662 def do_offset_and_free(self, bm, me, offset_infos=None, set_edges_orig=None): 663 # If offset_infos is None, use caches. 664 # Makes caches invalid after offset. 665 666 #time = perf_counter() 667 668 if offset_infos is None: 669 # using cache 670 bmverts = tuple(bm.verts) 671 bmedges = tuple(bm.edges) 672 edges_orig = [bmedges[ix] for ix in self._cache_edges_orig_ixs] 673 verts_directions = [] 674 for ix_vs, directions in self._cache_offset_infos: 675 verts = tuple(bmverts[ix] for ix in ix_vs) 676 verts_directions.append((verts, directions)) 677 else: 678 verts_directions = offset_infos 679 edges_orig = list(set_edges_orig) 680 681 if self.depth_mode == 'angle': 682 w = self.width if not self.flip_width else -self.width 683 angle = self.angle if not self.flip_angle else -self.angle 684 width = w * cos(angle) 685 depth = w * sin(angle) 686 else: 687 width = self.width if not self.flip_width else -self.width 688 depth = self.depth if not self.flip_depth else -self.depth 689 690 # Extrude 691 if self.geometry_mode == 'move': 692 geom_ex = None 693 else: 694 geom_ex = extrude_edges(bm, edges_orig) 695 696 for verts, directions in verts_directions: 697 move_verts(width, depth, verts, directions, geom_ex) 698 699 clean(bm, self.geometry_mode, edges_orig, geom_ex) 700 701 bpy.ops.object.mode_set(mode="OBJECT") 702 bm.to_mesh(me) 703 bpy.ops.object.mode_set(mode="EDIT") 704 bm.free() 705 self.caches_valid = False # Make caches invalid. 706 707 #print("OffsetEdges offset: ", perf_counter() - time) 708 709 def execute(self, context): 710 # In edit mode 711 edit_object = context.edit_object 712 bpy.ops.object.mode_set(mode="OBJECT") 713 714 me = edit_object.data 715 bm = bmesh.new() 716 bm.from_mesh(me) 717 718 offset_infos, edges_orig = self.get_offset_infos(bm, edit_object) 719 if offset_infos is False: 720 bpy.ops.object.mode_set(mode="EDIT") 721 return {'CANCELLED'} 722 723 self.do_offset_and_free(bm, me, offset_infos, edges_orig) 724 725 return {'FINISHED'} 726 727 def restore_original_and_free(self, context): 728 self.caches_valid = False # Make caches invalid. 729 context.area.header_text_set() 730 731 me = context.edit_object.data 732 bpy.ops.object.mode_set(mode="OBJECT") 733 self._bm_orig.to_mesh(me) 734 bpy.ops.object.mode_set(mode="EDIT") 735 736 self._bm_orig.free() 737 context.area.header_text_set() 738 739 def invoke(self, context, event): 740 # In edit mode 741 edit_object = context.edit_object 742 me = edit_object.data 743 bpy.ops.object.mode_set(mode="OBJECT") 744 for p in me.polygons: 745 if p.select: 746 self.follow_face = True 747 break 748 749 self.caches_valid = False 750 bpy.ops.object.mode_set(mode="EDIT") 751 return self.execute(context) 752 753class OffsetEdgesMenu(bpy.types.Menu): 754 bl_idname = "VIEW3D_MT_edit_mesh_offset_edges" 755 bl_label = "Offset Edges" 756 757 def draw(self, context): 758 layout = self.layout 759 layout.operator_context = 'INVOKE_DEFAULT' 760 761 off = layout.operator('mesh.offset_edges', text='Offset') 762 off.geometry_mode = 'offset' 763 764 ext = layout.operator('mesh.offset_edges', text='Extrude') 765 ext.geometry_mode = 'extrude' 766 767 mov = layout.operator('mesh.offset_edges', text='Move') 768 mov.geometry_mode = 'move' 769 770classes = ( 771OffsetEdges, 772OffsetEdgesMenu, 773) 774 775def draw_item(self, context): 776 self.layout.menu("VIEW3D_MT_edit_mesh_offset_edges") 777 778 779def register(): 780 for cls in classes: 781 bpy.utils.register_class(cls) 782 bpy.types.VIEW3D_MT_edit_mesh_edges.prepend(draw_item) 783 784 785def unregister(): 786 for cls in reversed(classes): 787 bpy.utils.unregister_class(cls) 788 bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item) 789 790 791if __name__ == '__main__': 792 register() 793