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