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