Update for 2.8
[blender-addons.git] / mesh_extra_tools / mesh_offset_edges.py
blob19eccd3b1c96af34a9a05b65b887e83c4b52d5ce
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Offset Edges",
21 "author": "Hidesato Ikeya",
22 "version": (0, 2, 6),
23 "blender": (2, 70, 0),
24 "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges",
25 "description": "Offset Edges",
26 "warning": "",
27 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/"
28 "Py/Scripts/Modeling/offset_edges",
29 "category": "Mesh"}
31 import bpy
32 import bmesh
33 from bpy.types import Operator
34 from math import sin, cos, pi, radians
35 from mathutils import Vector
36 from time import perf_counter
38 from bpy.props import (
39 BoolProperty,
40 FloatProperty,
41 EnumProperty,
44 # Globals
45 X_UP = Vector((1.0, .0, .0))
46 Y_UP = Vector((.0, 1.0, .0))
47 Z_UP = Vector((.0, .0, 1.0))
48 ZERO_VEC = Vector((.0, .0, .0))
49 ANGLE_90 = pi / 2
50 ANGLE_180 = pi
51 ANGLE_360 = 2 * pi
53 # switch performance logging
54 ENABLE_DEBUG = False
57 def calc_loop_normal(verts, fallback=Z_UP):
58 # Calculate normal from verts using Newell's method
59 normal = ZERO_VEC.copy()
61 if verts[0] is verts[-1]:
62 # Perfect loop
63 range_verts = range(1, len(verts))
64 else:
65 # Half loop
66 range_verts = range(0, len(verts))
68 for i in range_verts:
69 v1co, v2co = verts[i - 1].co, verts[i].co
70 normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z)
71 normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x)
72 normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y)
74 if normal != ZERO_VEC:
75 normal.normalize()
76 else:
77 normal = fallback
79 return normal
82 def collect_edges(bm):
83 set_edges_orig = set()
84 for e in bm.edges:
85 if e.select:
86 co_faces_selected = 0
87 for f in e.link_faces:
88 if f.select:
89 co_faces_selected += 1
90 if co_faces_selected == 2:
91 break
92 else:
93 set_edges_orig.add(e)
95 if not set_edges_orig:
96 return None
98 return set_edges_orig
101 def collect_loops(set_edges_orig):
102 set_edges_copy = set_edges_orig.copy()
104 loops = [] # [v, e, v, e, ... , e, v]
105 while set_edges_copy:
106 edge_start = set_edges_copy.pop()
107 v_left, v_right = edge_start.verts
108 lp = [v_left, edge_start, v_right]
109 reverse = False
110 while True:
111 edge = None
112 for e in v_right.link_edges:
113 if e in set_edges_copy:
114 if edge:
115 # Overlap detected.
116 return None
117 edge = e
118 set_edges_copy.remove(e)
119 if edge:
120 v_right = edge.other_vert(v_right)
121 lp.extend((edge, v_right))
122 continue
123 else:
124 if v_right is v_left:
125 # Real loop.
126 loops.append(lp)
127 break
128 elif reverse is False:
129 # Right side of half loop
130 # Reversing the loop to operate same procedure on the left side
131 lp.reverse()
132 v_right, v_left = v_left, v_right
133 reverse = True
134 continue
135 else:
136 # Half loop, completed
137 loops.append(lp)
138 break
139 return loops
142 def get_adj_ix(ix_start, vec_edges, half_loop):
143 # Get adjacent edge index, skipping zero length edges
144 len_edges = len(vec_edges)
145 if half_loop:
146 range_right = range(ix_start, len_edges)
147 range_left = range(ix_start - 1, -1, -1)
148 else:
149 range_right = range(ix_start, ix_start + len_edges)
150 range_left = range(ix_start - 1, ix_start - 1 - len_edges, -1)
152 ix_right = ix_left = None
153 for i in range_right:
154 # Right
155 i %= len_edges
156 if vec_edges[i] != ZERO_VEC:
157 ix_right = i
158 break
159 for i in range_left:
160 # Left
161 i %= len_edges
162 if vec_edges[i] != ZERO_VEC:
163 ix_left = i
164 break
165 if half_loop:
166 # If index of one side is None, assign another index
167 if ix_right is None:
168 ix_right = ix_left
169 if ix_left is None:
170 ix_left = ix_right
172 return ix_right, ix_left
175 def get_adj_faces(edges):
176 adj_faces = []
177 for e in edges:
178 adj_f = None
179 co_adj = 0
180 for f in e.link_faces:
181 # Search an adjacent face
182 # Selected face has precedence
183 if not f.hide and f.normal != ZERO_VEC:
184 adj_f = f
185 co_adj += 1
186 if f.select:
187 adj_faces.append(adj_f)
188 break
189 else:
190 if co_adj == 1:
191 adj_faces.append(adj_f)
192 else:
193 adj_faces.append(None)
194 return adj_faces
197 def get_edge_rail(vert, set_edges_orig):
198 co_edges = co_edges_selected = 0
199 vec_inner = None
200 for e in vert.link_edges:
201 if (e not in set_edges_orig and
202 (e.select or (co_edges_selected == 0 and not e.hide))):
203 v_other = e.other_vert(vert)
204 vec = v_other.co - vert.co
205 if vec != ZERO_VEC:
206 vec_inner = vec
207 if e.select:
208 co_edges_selected += 1
209 if co_edges_selected == 2:
210 return None
211 else:
212 co_edges += 1
213 if co_edges_selected == 1:
214 vec_inner.normalize()
215 return vec_inner
216 elif co_edges == 1:
217 # No selected edges, one unselected edge
218 vec_inner.normalize()
219 return vec_inner
220 else:
221 return None
224 def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l):
225 # Cross rail is a cross vector between normal_r and normal_l
226 vec_cross = normal_r.cross(normal_l)
227 if vec_cross.dot(vec_tan) < .0:
228 vec_cross *= -1
229 cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l))
230 cos = vec_tan.dot(vec_cross)
231 if cos >= cos_min:
232 vec_cross.normalize()
233 return vec_cross
234 else:
235 return None
238 def move_verts(width, depth, verts, directions, geom_ex):
239 if geom_ex:
240 geom_s = geom_ex['side']
241 verts_ex = []
242 for v in verts:
243 for e in v.link_edges:
244 if e in geom_s:
245 verts_ex.append(e.other_vert(v))
246 break
247 verts = verts_ex
249 for v, (vec_width, vec_depth) in zip(verts, directions):
250 v.co += width * vec_width + depth * vec_depth
253 def extrude_edges(bm, edges_orig):
254 extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom']
255 n_edges = n_faces = len(edges_orig)
256 n_verts = len(extruded) - n_edges - n_faces
258 geom = dict()
259 geom['verts'] = verts = set(extruded[:n_verts])
260 geom['edges'] = edges = set(extruded[n_verts:n_verts + n_edges])
261 geom['faces'] = set(extruded[n_verts + n_edges:])
262 geom['side'] = set(e for v in verts for e in v.link_edges if e not in edges)
264 return geom
267 def clean(bm, mode, edges_orig, geom_ex=None):
268 for f in bm.faces:
269 f.select = False
270 if geom_ex:
271 for e in geom_ex['edges']:
272 e.select = True
273 if mode == 'offset':
274 lis_geom = list(geom_ex['side']) + list(geom_ex['faces'])
275 bmesh.ops.delete(bm, geom=lis_geom, context=2)
276 else:
277 for e in edges_orig:
278 e.select = True
281 def collect_mirror_planes(edit_object):
282 mirror_planes = []
283 eob_mat_inv = edit_object.matrix_world.inverted()
284 for m in edit_object.modifiers:
285 if (m.type == 'MIRROR' and m.use_mirror_merge):
286 merge_limit = m.merge_threshold
287 if not m.mirror_object:
288 loc = ZERO_VEC
289 norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP
290 else:
291 mirror_mat_local = eob_mat_inv * m.mirror_object.matrix_world
292 loc = mirror_mat_local.to_translation()
293 norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated()
294 norm_x = norm_x.to_3d().normalized()
295 norm_y = norm_y.to_3d().normalized()
296 norm_z = norm_z.to_3d().normalized()
297 if m.use_x:
298 mirror_planes.append((loc, norm_x, merge_limit))
299 if m.use_y:
300 mirror_planes.append((loc, norm_y, merge_limit))
301 if m.use_z:
302 mirror_planes.append((loc, norm_z, merge_limit))
303 return mirror_planes
306 def get_vert_mirror_pairs(set_edges_orig, mirror_planes):
307 if mirror_planes:
308 set_edges_copy = set_edges_orig.copy()
309 vert_mirror_pairs = dict()
310 for e in set_edges_orig:
311 v1, v2 = e.verts
312 for mp in mirror_planes:
313 p_co, p_norm, mlimit = mp
314 v1_dist = abs(p_norm.dot(v1.co - p_co))
315 v2_dist = abs(p_norm.dot(v2.co - p_co))
316 if v1_dist <= mlimit:
317 # v1 is on a mirror plane
318 vert_mirror_pairs[v1] = mp
319 if v2_dist <= mlimit:
320 # v2 is on a mirror plane
321 vert_mirror_pairs[v2] = mp
322 if v1_dist <= mlimit and v2_dist <= mlimit:
323 # This edge is on a mirror_plane, so should not be offsetted
324 set_edges_copy.remove(e)
325 return vert_mirror_pairs, set_edges_copy
326 else:
327 return None, set_edges_orig
330 def get_mirror_rail(mirror_plane, vec_up):
331 p_norm = mirror_plane[1]
332 mirror_rail = vec_up.cross(p_norm)
333 if mirror_rail != ZERO_VEC:
334 mirror_rail.normalize()
335 # Project vec_up to mirror_plane
336 vec_up = vec_up - vec_up.project(p_norm)
337 vec_up.normalize()
338 return mirror_rail, vec_up
339 else:
340 return None, vec_up
343 def reorder_loop(verts, edges, lp_normal, adj_faces):
344 for i, adj_f in enumerate(adj_faces):
345 if adj_f is None:
346 continue
348 v1, v2 = verts[i], verts[i + 1]
349 fv = tuple(adj_f.verts)
350 if fv[fv.index(v1) - 1] is v2:
351 # Align loop direction
352 verts.reverse()
353 edges.reverse()
354 adj_faces.reverse()
356 if lp_normal.dot(adj_f.normal) < .0:
357 lp_normal *= -1
358 break
359 else:
360 # All elements in adj_faces are None
361 for v in verts:
362 if v.normal != ZERO_VEC:
363 if lp_normal.dot(v.normal) < .0:
364 verts.reverse()
365 edges.reverse()
366 lp_normal *= -1
367 break
369 return verts, edges, lp_normal, adj_faces
372 def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options):
373 opt_follow_face = options['follow_face']
374 opt_edge_rail = options['edge_rail']
375 opt_er_only_end = options['edge_rail_only_end']
376 opt_threshold = options['threshold']
378 verts, edges = lp[::2], lp[1::2]
379 set_edges = set(edges)
380 lp_normal = calc_loop_normal(verts, fallback=normal_fallback)
382 # Loop order might be changed below
383 if lp_normal.dot(vec_upward) < .0:
384 # Make this loop's normal towards vec_upward
385 verts.reverse()
386 edges.reverse()
387 lp_normal *= -1
389 if opt_follow_face:
390 adj_faces = get_adj_faces(edges)
391 verts, edges, lp_normal, adj_faces = \
392 reorder_loop(verts, edges, lp_normal, adj_faces)
393 else:
394 adj_faces = (None, ) * len(edges)
395 # Loop order might be changed above
397 vec_edges = tuple((e.other_vert(v).co - v.co).normalized()
398 for v, e in zip(verts, edges))
400 if verts[0] is verts[-1]:
401 # Real loop. Popping last vertex
402 verts.pop()
403 HALF_LOOP = False
404 else:
405 # Half loop
406 HALF_LOOP = True
408 len_verts = len(verts)
409 directions = []
410 for i in range(len_verts):
411 vert = verts[i]
412 ix_right, ix_left = i, i - 1
414 VERT_END = False
415 if HALF_LOOP:
416 if i == 0:
417 # First vert
418 ix_left = ix_right
419 VERT_END = True
420 elif i == len_verts - 1:
421 # Last vert
422 ix_right = ix_left
423 VERT_END = True
425 edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left]
426 face_right, face_left = adj_faces[ix_right], adj_faces[ix_left]
428 norm_right = face_right.normal if face_right else lp_normal
429 norm_left = face_left.normal if face_left else lp_normal
430 if norm_right.angle(norm_left) > opt_threshold:
431 # Two faces are not flat
432 two_normals = True
433 else:
434 two_normals = False
436 tan_right = edge_right.cross(norm_right).normalized()
437 tan_left = edge_left.cross(norm_left).normalized()
438 tan_avr = (tan_right + tan_left).normalized()
439 norm_avr = (norm_right + norm_left).normalized()
441 rail = None
442 if two_normals or opt_edge_rail:
443 # Get edge rail
444 # edge rail is a vector of an inner edge
445 if two_normals or (not opt_er_only_end) or VERT_END:
446 rail = get_edge_rail(vert, set_edges)
447 if vert_mirror_pairs and VERT_END:
448 if vert in vert_mirror_pairs:
449 rail, norm_avr = get_mirror_rail(vert_mirror_pairs[vert], norm_avr)
450 if (not rail) and two_normals:
451 # Get cross rail
452 # Cross rail is a cross vector between norm_right and norm_left
453 rail = get_cross_rail(
454 tan_avr, edge_right, edge_left, norm_right, norm_left)
455 if rail:
456 dot = tan_avr.dot(rail)
457 if dot > .0:
458 tan_avr = rail
459 elif dot < .0:
460 tan_avr = -rail
462 vec_plane = norm_avr.cross(tan_avr)
463 e_dot_p_r = edge_right.dot(vec_plane)
464 e_dot_p_l = edge_left.dot(vec_plane)
465 if e_dot_p_r or e_dot_p_l:
466 if e_dot_p_r > e_dot_p_l:
467 vec_edge, e_dot_p = edge_right, e_dot_p_r
468 else:
469 vec_edge, e_dot_p = edge_left, e_dot_p_l
471 vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized()
472 # Make vec_tan perpendicular to vec_edge
473 vec_up = vec_tan.cross(vec_edge)
475 vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge
476 vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge
477 else:
478 vec_width = tan_avr
479 vec_depth = norm_avr
481 directions.append((vec_width, vec_depth))
483 return verts, directions
486 angle_presets = {'0°': 0,
487 '15°': radians(15),
488 '30°': radians(30),
489 '45°': radians(45),
490 '60°': radians(60),
491 '75°': radians(75),
492 '90°': radians(90),
496 def use_cashes(self, context):
497 self.caches_valid = True
500 def assign_angle_presets(self, context):
501 use_cashes(self, context)
502 self.angle = angle_presets[self.angle_presets]
505 class OffsetEdges(Operator):
506 bl_idname = "mesh.offset_edges"
507 bl_label = "Offset Edges"
508 bl_description = ("Extrude, Move or Offset the selected Edges\n"
509 "Operates only on separate Edge loops selections")
510 bl_options = {'REGISTER', 'UNDO'}
512 geometry_mode = EnumProperty(
513 items=[('offset', "Offset", "Offset edges"),
514 ('extrude', "Extrude", "Extrude edges"),
515 ('move', "Move", "Move selected edges")],
516 name="Geometry mode",
517 default='offset',
518 update=use_cashes
520 width = FloatProperty(
521 name="Width",
522 default=.2,
523 precision=4, step=1,
524 update=use_cashes
526 flip_width = BoolProperty(
527 name="Flip Width",
528 default=False,
529 description="Flip width direction",
530 update=use_cashes
532 depth = FloatProperty(
533 name="Depth",
534 default=.0,
535 precision=4, step=1,
536 update=use_cashes
538 flip_depth = BoolProperty(
539 name="Flip Depth",
540 default=False,
541 description="Flip depth direction",
542 update=use_cashes
544 depth_mode = EnumProperty(
545 items=[('angle', "Angle", "Angle"),
546 ('depth', "Depth", "Depth")],
547 name="Depth mode",
548 default='angle',
549 update=use_cashes
551 angle = FloatProperty(
552 name="Angle", default=0,
553 precision=3, step=.1,
554 min=-2 * pi, max=2 * pi,
555 subtype='ANGLE',
556 description="Angle",
557 update=use_cashes
559 flip_angle = BoolProperty(
560 name="Flip Angle",
561 default=False,
562 description="Flip Angle",
563 update=use_cashes
565 follow_face = BoolProperty(
566 name="Follow Face",
567 default=False,
568 description="Offset along faces around"
570 mirror_modifier = BoolProperty(
571 name="Mirror Modifier",
572 default=False,
573 description="Take into account of Mirror modifier"
575 edge_rail = BoolProperty(
576 name="Edge Rail",
577 default=False,
578 description="Align vertices along inner edges"
580 edge_rail_only_end = BoolProperty(
581 name="Edge Rail Only End",
582 default=False,
583 description="Apply edge rail to end verts only"
585 threshold = FloatProperty(
586 name="Flat Face Threshold",
587 default=radians(0.05), precision=5,
588 step=1.0e-4, subtype='ANGLE',
589 description="If difference of angle between two adjacent faces is "
590 "below this value, those faces are regarded as flat",
591 options={'HIDDEN'}
593 caches_valid = BoolProperty(
594 name="Caches Valid",
595 default=False,
596 options={'HIDDEN'}
598 angle_presets = EnumProperty(
599 items=[('0°', "0°", "0°"),
600 ('15°', "15°", "15°"),
601 ('30°', "30°", "30°"),
602 ('45°', "45°", "45°"),
603 ('60°', "60°", "60°"),
604 ('75°', "75°", "75°"),
605 ('90°', "90°", "90°"), ],
606 name="Angle Presets",
607 default='0°',
608 update=assign_angle_presets
611 _cache_offset_infos = None
612 _cache_edges_orig_ixs = None
614 @classmethod
615 def poll(self, context):
616 return context.mode == 'EDIT_MESH'
618 def draw(self, context):
619 layout = self.layout
620 layout.prop(self, 'geometry_mode', text="")
622 row = layout.row(align=True)
623 row.prop(self, 'width')
624 row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)
625 layout.prop(self, 'depth_mode', expand=True)
627 if self.depth_mode == 'angle':
628 d_mode = 'angle'
629 flip = 'flip_angle'
630 else:
631 d_mode = 'depth'
632 flip = 'flip_depth'
633 row = layout.row(align=True)
634 row.prop(self, d_mode)
635 row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True)
636 if self.depth_mode == 'angle':
637 layout.prop(self, 'angle_presets', text="Presets", expand=True)
639 layout.separator()
641 layout.prop(self, 'follow_face')
643 row = layout.row()
644 row.prop(self, 'edge_rail')
645 if self.edge_rail:
646 row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)
648 layout.prop(self, 'mirror_modifier')
649 layout.operator('mesh.offset_edges', text="Repeat")
651 if self.follow_face:
652 layout.separator()
653 layout.prop(self, 'threshold', text="Threshold")
655 def get_offset_infos(self, bm, edit_object):
656 if self.caches_valid and self._cache_offset_infos is not None:
657 # Return None, indicating to use cache
658 return None, None
660 if ENABLE_DEBUG:
661 time = perf_counter()
663 set_edges_orig = collect_edges(bm)
664 if set_edges_orig is None:
665 self.report({'WARNING'},
666 "No edges selected or edge loops could not be determined")
667 return False, False
669 if self.mirror_modifier:
670 mirror_planes = collect_mirror_planes(edit_object)
671 vert_mirror_pairs, set_edges = \
672 get_vert_mirror_pairs(set_edges_orig, mirror_planes)
674 if set_edges:
675 set_edges_orig = set_edges
676 else:
677 vert_mirror_pairs = None
678 else:
679 vert_mirror_pairs = None
681 loops = collect_loops(set_edges_orig)
682 if loops is None:
683 self.report({'WARNING'},
684 "Overlap detected. Select non-overlapping edge loops")
685 return False, False
687 vec_upward = (X_UP + Y_UP + Z_UP).normalized()
688 # vec_upward is used to unify loop normals when follow_face is off
689 normal_fallback = Z_UP
690 # normal_fallback = Vector(context.region_data.view_matrix[2][:3])
691 # normal_fallback is used when loop normal cannot be calculated
693 follow_face = self.follow_face
694 edge_rail = self.edge_rail
695 er_only_end = self.edge_rail_only_end
696 threshold = self.threshold
698 offset_infos = []
699 for lp in loops:
700 verts, directions = get_directions(
701 lp, vec_upward, normal_fallback, vert_mirror_pairs,
702 follow_face=follow_face, edge_rail=edge_rail,
703 edge_rail_only_end=er_only_end,
704 threshold=threshold)
705 if verts:
706 offset_infos.append((verts, directions))
708 # Saving caches
709 self._cache_offset_infos = _cache_offset_infos = []
710 for verts, directions in offset_infos:
711 v_ixs = tuple(v.index for v in verts)
712 _cache_offset_infos.append((v_ixs, directions))
713 self._cache_edges_orig_ixs = tuple(e.index for e in set_edges_orig)
715 if ENABLE_DEBUG:
716 print("Preparing OffsetEdges: ", perf_counter() - time)
718 return offset_infos, set_edges_orig
720 def do_offset_and_free(self, bm, me, offset_infos=None, set_edges_orig=None):
721 # If offset_infos is None, use caches
722 # Makes caches invalid after offset
724 if ENABLE_DEBUG:
725 time = perf_counter()
727 if offset_infos is None:
728 # using cache
729 bmverts = tuple(bm.verts)
730 bmedges = tuple(bm.edges)
731 edges_orig = [bmedges[ix] for ix in self._cache_edges_orig_ixs]
732 verts_directions = []
733 for ix_vs, directions in self._cache_offset_infos:
734 verts = tuple(bmverts[ix] for ix in ix_vs)
735 verts_directions.append((verts, directions))
736 else:
737 verts_directions = offset_infos
738 edges_orig = list(set_edges_orig)
740 if self.depth_mode == 'angle':
741 w = self.width if not self.flip_width else -self.width
742 angle = self.angle if not self.flip_angle else -self.angle
743 width = w * cos(angle)
744 depth = w * sin(angle)
745 else:
746 width = self.width if not self.flip_width else -self.width
747 depth = self.depth if not self.flip_depth else -self.depth
749 # Extrude
750 if self.geometry_mode == 'move':
751 geom_ex = None
752 else:
753 geom_ex = extrude_edges(bm, edges_orig)
755 for verts, directions in verts_directions:
756 move_verts(width, depth, verts, directions, geom_ex)
758 clean(bm, self.geometry_mode, edges_orig, geom_ex)
760 bpy.ops.object.mode_set(mode="OBJECT")
761 bm.to_mesh(me)
762 bpy.ops.object.mode_set(mode="EDIT")
763 bm.free()
764 self.caches_valid = False # Make caches invalid
766 if ENABLE_DEBUG:
767 print("OffsetEdges offset: ", perf_counter() - time)
769 def execute(self, context):
770 # In edit mode
771 edit_object = context.edit_object
772 bpy.ops.object.mode_set(mode="OBJECT")
774 me = edit_object.data
775 bm = bmesh.new()
776 bm.from_mesh(me)
778 offset_infos, edges_orig = self.get_offset_infos(bm, edit_object)
779 if offset_infos is False:
780 bpy.ops.object.mode_set(mode="EDIT")
781 return {'CANCELLED'}
783 self.do_offset_and_free(bm, me, offset_infos, edges_orig)
785 return {'FINISHED'}
787 def restore_original_and_free(self, context):
788 self.caches_valid = False # Make caches invalid
789 context.area.header_text_set("")
791 me = context.edit_object.data
792 bpy.ops.object.mode_set(mode="OBJECT")
793 self._bm_orig.to_mesh(me)
794 bpy.ops.object.mode_set(mode="EDIT")
796 self._bm_orig.free()
797 context.area.header_text_set("")
799 def invoke(self, context, event):
800 # In edit mode
801 edit_object = context.edit_object
802 me = edit_object.data
803 bpy.ops.object.mode_set(mode="OBJECT")
804 for p in me.polygons:
805 if p.select:
806 self.follow_face = True
807 break
809 self.caches_valid = False
810 bpy.ops.object.mode_set(mode="EDIT")
811 return self.execute(context)
814 def register():
815 bpy.utils.register_module(__name__)
818 def unregister():
819 bpy.utils.unregister_module(__name__)
822 if __name__ == '__main__':
823 register()