Cleanup: tabs -> spaces
[blender-addons.git] / mesh_tools / mesh_offset_edges.py
blob1471d256568277317405cbd99d4841fbf52c4c59
1 # ***** BEGIN GPL LICENSE BLOCK *****
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.
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.
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.
18 # ***** END GPL LICENCE BLOCK *****
20 bl_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",
34 import math
35 from math import sin, cos, pi, copysign, radians
36 import bpy
37 from bpy_extras import view3d_utils
38 import bmesh
39 from mathutils import Vector
40 from time import perf_counter
42 X_UP = Vector((1.0, .0, .0))
43 Y_UP = Vector((.0, 1.0, .0))
44 Z_UP = Vector((.0, .0, 1.0))
45 ZERO_VEC = Vector((.0, .0, .0))
46 ANGLE_90 = pi / 2
47 ANGLE_180 = pi
48 ANGLE_360 = 2 * pi
51 def calc_loop_normal(verts, fallback=Z_UP):
52 # Calculate normal from verts using Newell's method.
53 normal = ZERO_VEC.copy()
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))
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)
68 if normal != ZERO_VEC:
69 normal.normalize()
70 else:
71 normal = fallback
73 return normal
75 def 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)
88 if not set_edges_orig:
89 return None
91 return set_edges_orig
93 def collect_loops(set_edges_orig):
94 set_edges_copy = set_edges_orig.copy()
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
133 def 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)
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
163 return ix_right, ix_left
165 def 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
188 def 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
214 def 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.
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
228 def 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
240 for v, (vec_width, vec_depth) in zip(verts, directions):
241 v.co += width * vec_width + depth * vec_depth
243 def 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
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)
254 return geom
256 def 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
269 def collect_mirror_planes(edit_object):
270 mirror_planes = []
271 eob_mat_inv = edit_object.matrix_world.inverted()
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
295 def 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
318 def 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
330 def 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
355 return verts, edges, lp_normal, adj_faces
357 def 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']
363 verts, edges = lp[::2], lp[1::2]
364 set_edges = set(edges)
365 lp_normal = calc_loop_normal(verts, fallback=normal_fallback)
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
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.
382 vec_edges = tuple((e.other_vert(v).co - v.co).normalized()
383 for v, e in zip(verts, edges))
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
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
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
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]
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
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()
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
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
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)
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
467 directions.append((vec_width, vec_depth))
469 return verts, directions
471 def use_cashes(self, context):
472 self.caches_valid = True
474 angle_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),}
481 def assign_angle_presets(self, context):
482 use_cashes(self, context)
483 self.angle = angle_presets[self.angle_presets]
485 class OffsetEdges(bpy.types.Operator):
486 """Offset Edges."""
487 bl_idname = "mesh.offset_edges"
488 bl_label = "Offset Edges"
489 bl_options = {'REGISTER', 'UNDO'}
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)
550 _cache_offset_infos = None
551 _cache_edges_orig_ixs = None
553 @classmethod
554 def poll(self, context):
555 return context.mode == 'EDIT_MESH'
557 def draw(self, context):
558 layout = self.layout
559 layout.prop(self, 'geometry_mode', text="")
560 #layout.prop(self, 'geometry_mode', expand=True)
562 row = layout.row(align=True)
563 row.prop(self, 'width')
564 row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)
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)
579 layout.separator()
581 layout.prop(self, 'follow_face')
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)
588 layout.prop(self, 'mirror_modifier')
590 #layout.operator('mesh.offset_edges', text='Repeat')
592 if self.follow_face:
593 layout.separator()
594 layout.prop(self, 'threshold', text='Threshold')
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
602 time = perf_counter()
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
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)
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
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
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.
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
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))
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)
658 print("Preparing OffsetEdges: ", perf_counter() - time)
660 return offset_infos, set_edges_orig
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.
666 #time = perf_counter()
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)
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
690 # Extrude
691 if self.geometry_mode == 'move':
692 geom_ex = None
693 else:
694 geom_ex = extrude_edges(bm, edges_orig)
696 for verts, directions in verts_directions:
697 move_verts(width, depth, verts, directions, geom_ex)
699 clean(bm, self.geometry_mode, edges_orig, geom_ex)
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.
707 #print("OffsetEdges offset: ", perf_counter() - time)
709 def execute(self, context):
710 # In edit mode
711 edit_object = context.edit_object
712 bpy.ops.object.mode_set(mode="OBJECT")
714 me = edit_object.data
715 bm = bmesh.new()
716 bm.from_mesh(me)
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'}
723 self.do_offset_and_free(bm, me, offset_infos, edges_orig)
725 return {'FINISHED'}
727 def restore_original_and_free(self, context):
728 self.caches_valid = False # Make caches invalid.
729 context.area.header_text_set()
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")
736 self._bm_orig.free()
737 context.area.header_text_set()
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
749 self.caches_valid = False
750 bpy.ops.object.mode_set(mode="EDIT")
751 return self.execute(context)
753 class OffsetEdgesMenu(bpy.types.Menu):
754 bl_idname = "VIEW3D_MT_edit_mesh_offset_edges"
755 bl_label = "Offset Edges"
757 def draw(self, context):
758 layout = self.layout
759 layout.operator_context = 'INVOKE_DEFAULT'
761 off = layout.operator('mesh.offset_edges', text='Offset')
762 off.geometry_mode = 'offset'
764 ext = layout.operator('mesh.offset_edges', text='Extrude')
765 ext.geometry_mode = 'extrude'
767 mov = layout.operator('mesh.offset_edges', text='Move')
768 mov.geometry_mode = 'move'
770 classes = (
771 OffsetEdges,
772 OffsetEdgesMenu,
775 def draw_item(self, context):
776 self.layout.menu("VIEW3D_MT_edit_mesh_offset_edges")
779 def register():
780 for cls in classes:
781 bpy.utils.register_class(cls)
782 bpy.types.VIEW3D_MT_edit_mesh_edges.prepend(draw_item)
785 def unregister():
786 for cls in reversed(classes):
787 bpy.utils.unregister_class(cls)
788 bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)
791 if __name__ == '__main__':
792 register()