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