Skip to content
Snippets Groups Projects
mesh_offset_edges.py 26.17 KiB
# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****

bl_info = {
    "name": "Offset Edges",
    "author": "Hidesato Ikeya, Veezen fix 2.8 (temporary)",
	#i tried edit newest version, but got some errors, works only on 0,2,6
    "version": (0, 2, 6),
    "blender": (2, 80, 0),
    "location": "VIEW3D > Edge menu(CTRL-E) > Offset Edges",
    "description": "Offset Edges",
    "warning": "",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Modeling/offset_edges",
    "tracker_url": "",
    "category": "Mesh"}

import math
from math import sin, cos, pi, copysign, radians
import bpy
from bpy_extras import view3d_utils
import bmesh
from mathutils import Vector
from time import perf_counter

X_UP = Vector((1.0, .0, .0))
Y_UP = Vector((.0, 1.0, .0))
Z_UP = Vector((.0, .0, 1.0))
ZERO_VEC = Vector((.0, .0, .0))
ANGLE_90 = pi / 2
ANGLE_180 = pi
ANGLE_360 = 2 * pi


def calc_loop_normal(verts, fallback=Z_UP):
    # Calculate normal from verts using Newell's method.
    normal = ZERO_VEC.copy()

    if verts[0] is verts[-1]:
        # Perfect loop
        range_verts = range(1, len(verts))
    else:
        # Half loop
        range_verts = range(0, len(verts))

    for i in range_verts:
        v1co, v2co = verts[i-1].co, verts[i].co
        normal.x += (v1co.y - v2co.y) * (v1co.z + v2co.z)
        normal.y += (v1co.z - v2co.z) * (v1co.x + v2co.x)
        normal.z += (v1co.x - v2co.x) * (v1co.y + v2co.y)

    if normal != ZERO_VEC:
        normal.normalize()
    else:
        normal = fallback

    return normal

def collect_edges(bm):
    set_edges_orig = set()
    for e in bm.edges:
        if e.select:
            co_faces_selected = 0
            for f in e.link_faces:
                if f.select:
                    co_faces_selected += 1
                    if co_faces_selected == 2:
                        break
            else:
                set_edges_orig.add(e)

    if not set_edges_orig:
        return None

    return set_edges_orig

def collect_loops(set_edges_orig):
    set_edges_copy = set_edges_orig.copy()

    loops = []  # [v, e, v, e, ... , e, v]
    while set_edges_copy:
        edge_start = set_edges_copy.pop()
        v_left, v_right = edge_start.verts
        lp = [v_left, edge_start, v_right]
        reverse = False
        while True:
            edge = None
            for e in v_right.link_edges:
                if e in set_edges_copy:
                    if edge:
                        # Overlap detected.
                        return None
                    edge = e
                    set_edges_copy.remove(e)
            if edge:
                v_right = edge.other_vert(v_right)
                lp.extend((edge, v_right))
                continue
            else:
                if v_right is v_left:
                    # Real loop.
                    loops.append(lp)
                    break
                elif reverse is False:
                    # Right side of half loop.
                    # Reversing the loop to operate same procedure on the left side.
                    lp.reverse()
                    v_right, v_left = v_left, v_right
                    reverse = True
                    continue
                else:
                    # Half loop, completed.
                    loops.append(lp)
                    break
    return loops

def get_adj_ix(ix_start, vec_edges, half_loop):
    # Get adjacent edge index, skipping zero length edges
    len_edges = len(vec_edges)
    if half_loop:
        range_right = range(ix_start, len_edges)
        range_left = range(ix_start-1, -1, -1)
    else:
        range_right = range(ix_start, ix_start+len_edges)
        range_left = range(ix_start-1, ix_start-1-len_edges, -1)

    ix_right = ix_left = None
    for i in range_right:
        # Right
        i %= len_edges
        if vec_edges[i] != ZERO_VEC:
            ix_right = i
            break
    for i in range_left:
        # Left
        i %= len_edges
        if vec_edges[i] != ZERO_VEC:
            ix_left = i
            break
    if half_loop:
        # If index of one side is None, assign another index.
        if ix_right is None:
            ix_right = ix_left
        if ix_left is None:
            ix_left = ix_right

    return ix_right, ix_left

def get_adj_faces(edges):
    adj_faces = []
    for e in edges:
        adj_f = None
        co_adj = 0
        for f in e.link_faces:
            # Search an adjacent face.
            # Selected face has precedance.
            if not f.hide and f.normal != ZERO_VEC:
                adj_exist = True
                adj_f = f
                co_adj += 1
                if f.select:
                    adj_faces.append(adj_f)
                    break
        else:
            if co_adj == 1:
                adj_faces.append(adj_f)
            else:
                adj_faces.append(None)
    return adj_faces


def get_edge_rail(vert, set_edges_orig):
    co_edges = co_edges_selected = 0
    vec_inner = None
    for e in vert.link_edges:
        if (e not in set_edges_orig and
           (e.select or (co_edges_selected == 0 and not e.hide))):
            v_other = e.other_vert(vert)
            vec = v_other.co - vert.co
            if vec != ZERO_VEC:
                vec_inner = vec
                if e.select:
                    co_edges_selected += 1
                    if co_edges_selected == 2:
                        return None
                else:
                    co_edges += 1
    if co_edges_selected == 1:
        vec_inner.normalize()
        return vec_inner
    elif co_edges == 1:
        # No selected edges, one unselected edge.
        vec_inner.normalize()
        return vec_inner
    else:
        return None

def get_cross_rail(vec_tan, vec_edge_r, vec_edge_l, normal_r, normal_l):
    # Cross rail is a cross vector between normal_r and normal_l.

    vec_cross = normal_r.cross(normal_l)
    if vec_cross.dot(vec_tan) < .0:
        vec_cross *= -1
    cos_min = min(vec_tan.dot(vec_edge_r), vec_tan.dot(-vec_edge_l))
    cos = vec_tan.dot(vec_cross)
    if cos >= cos_min:
        vec_cross.normalize()
        return vec_cross
    else:
        return None

def move_verts(width, depth, verts, directions, geom_ex):
    if geom_ex:
        geom_s = geom_ex['side']
        verts_ex = []
        for v in verts:
            for e in v.link_edges:
                if e in geom_s:
                    verts_ex.append(e.other_vert(v))
                    break
        #assert len(verts) == len(verts_ex)
        verts = verts_ex

    for v, (vec_width, vec_depth) in zip(verts, directions):
        v.co += width * vec_width + depth * vec_depth

def extrude_edges(bm, edges_orig):
    extruded = bmesh.ops.extrude_edge_only(bm, edges=edges_orig)['geom']
    n_edges = n_faces = len(edges_orig)
    n_verts = len(extruded) - n_edges - n_faces

    geom = dict()
    geom['verts'] = verts = set(extruded[:n_verts])
    geom['edges'] = edges = set(extruded[n_verts:n_verts + n_edges])
    geom['faces'] = set(extruded[n_verts + n_edges:])
    geom['side'] = set(e for v in verts for e in v.link_edges if e not in edges)

    return geom

def clean(bm, mode, edges_orig, geom_ex=None):
    for f in bm.faces:
        f.select = False
    if geom_ex:
        for e in geom_ex['edges']:
            e.select = True
        if mode == 'offset':
            lis_geom = list(geom_ex['side']) + list(geom_ex['faces'])
            bmesh.ops.delete(bm, geom=lis_geom, context='EDGES')
    else:
        for e in edges_orig:
            e.select = True

def collect_mirror_planes(edit_object):
    mirror_planes = []
    eob_mat_inv = edit_object.matrix_world.inverted()


    for m in edit_object.modifiers:
        if (m.type == 'MIRROR' and m.use_mirror_merge):
            merge_limit = m.merge_threshold
            if not m.mirror_object:
                loc = ZERO_VEC
                norm_x, norm_y, norm_z = X_UP, Y_UP, Z_UP
            else:
                mirror_mat_local = eob_mat_inv @ m.mirror_object.matrix_world
                loc = mirror_mat_local.to_translation()
                norm_x, norm_y, norm_z, _ = mirror_mat_local.adjugated()
                norm_x = norm_x.to_3d().normalized()
                norm_y = norm_y.to_3d().normalized()
                norm_z = norm_z.to_3d().normalized()
            if m.use_axis[0]:
                mirror_planes.append((loc, norm_x, merge_limit))
            if m.use_axis[1]:
                mirror_planes.append((loc, norm_y, merge_limit))
            if m.use_axis[2]:
                mirror_planes.append((loc, norm_z, merge_limit))
    return mirror_planes

def get_vert_mirror_pairs(set_edges_orig, mirror_planes):
    if mirror_planes:
        set_edges_copy = set_edges_orig.copy()
        vert_mirror_pairs = dict()
        for e in set_edges_orig:
            v1, v2 = e.verts
            for mp in mirror_planes:
                p_co, p_norm, mlimit = mp
                v1_dist = abs(p_norm.dot(v1.co - p_co))
                v2_dist = abs(p_norm.dot(v2.co - p_co))
                if v1_dist <= mlimit:
                    # v1 is on a mirror plane.
                    vert_mirror_pairs[v1] = mp
                if v2_dist <= mlimit:
                    # v2 is on a mirror plane.
                    vert_mirror_pairs[v2] = mp
                if v1_dist <= mlimit and v2_dist <= mlimit:
                    # This edge is on a mirror_plane, so should not be offsetted.
                    set_edges_copy.remove(e)
        return vert_mirror_pairs, set_edges_copy
    else:
        return None, set_edges_orig

def get_mirror_rail(mirror_plane, vec_up):
    p_norm = mirror_plane[1]
    mirror_rail = vec_up.cross(p_norm)
    if mirror_rail != ZERO_VEC:
        mirror_rail.normalize()
        # Project vec_up to mirror_plane
        vec_up = vec_up - vec_up.project(p_norm)
        vec_up.normalize()
        return mirror_rail, vec_up
    else:
        return None, vec_up

def reorder_loop(verts, edges, lp_normal, adj_faces):
    for i, adj_f in enumerate(adj_faces):
        if adj_f is None:
            continue
        v1, v2 = verts[i], verts[i+1]
        e = edges[i]
        fv = tuple(adj_f.verts)
        if fv[fv.index(v1)-1] is v2:
            # Align loop direction
            verts.reverse()
            edges.reverse()
            adj_faces.reverse()
        if lp_normal.dot(adj_f.normal) < .0:
            lp_normal *= -1
        break
    else:
        # All elements in adj_faces are None
        for v in verts:
            if v.normal != ZERO_VEC:
                if lp_normal.dot(v.normal) < .0:
                    verts.reverse()
                    edges.reverse()
                    lp_normal *= -1
                break

    return verts, edges, lp_normal, adj_faces

def get_directions(lp, vec_upward, normal_fallback, vert_mirror_pairs, **options):
    opt_follow_face = options['follow_face']
    opt_edge_rail = options['edge_rail']
    opt_er_only_end = options['edge_rail_only_end']
    opt_threshold = options['threshold']

    verts, edges = lp[::2], lp[1::2]
    set_edges = set(edges)
    lp_normal = calc_loop_normal(verts, fallback=normal_fallback)

    ##### Loop order might be changed below.
    if lp_normal.dot(vec_upward) < .0:
        # Make this loop's normal towards vec_upward.
        verts.reverse()
        edges.reverse()
        lp_normal *= -1

    if opt_follow_face:
        adj_faces = get_adj_faces(edges)
        verts, edges, lp_normal, adj_faces = \
            reorder_loop(verts, edges, lp_normal, adj_faces)
    else:
        adj_faces = (None, ) * len(edges)
    ##### Loop order might be changed above.

    vec_edges = tuple((e.other_vert(v).co - v.co).normalized()
                      for v, e in zip(verts, edges))

    if verts[0] is verts[-1]:
        # Real loop. Popping last vertex.
        verts.pop()
        HALF_LOOP = False
    else:
        # Half loop
        HALF_LOOP = True

    len_verts = len(verts)
    directions = []
    for i in range(len_verts):
        vert = verts[i]
        ix_right, ix_left = i, i-1

        VERT_END = False
        if HALF_LOOP:
            if i == 0:
                # First vert
                ix_left = ix_right
                VERT_END = True
            elif i == len_verts - 1:
                # Last vert
                ix_right = ix_left
                VERT_END = True

        edge_right, edge_left = vec_edges[ix_right], vec_edges[ix_left]
        face_right, face_left = adj_faces[ix_right], adj_faces[ix_left]

        norm_right = face_right.normal if face_right else lp_normal
        norm_left = face_left.normal if face_left else lp_normal
        if norm_right.angle(norm_left) > opt_threshold:
            # Two faces are not flat.
            two_normals = True
        else:
            two_normals = False

        tan_right = edge_right.cross(norm_right).normalized()
        tan_left = edge_left.cross(norm_left).normalized()
        tan_avr = (tan_right + tan_left).normalized()
        norm_avr = (norm_right + norm_left).normalized()

        rail = None
        if two_normals or opt_edge_rail:
            # Get edge rail.
            # edge rail is a vector of an inner edge.
            if two_normals or (not opt_er_only_end) or VERT_END:
                rail = get_edge_rail(vert, set_edges)
        if vert_mirror_pairs and VERT_END:
            if vert in vert_mirror_pairs:
                rail, norm_avr = \
                    get_mirror_rail(vert_mirror_pairs[vert], norm_avr)
        if (not rail) and two_normals:
            # Get cross rail.
            # Cross rail is a cross vector between norm_right and norm_left.
            rail = get_cross_rail(
                tan_avr, edge_right, edge_left, norm_right, norm_left)
        if rail:
            dot = tan_avr.dot(rail)
            if dot > .0:
                tan_avr = rail
            elif dot < .0:
                tan_avr = -rail

        vec_plane = norm_avr.cross(tan_avr)
        e_dot_p_r = edge_right.dot(vec_plane)
        e_dot_p_l = edge_left.dot(vec_plane)
        if e_dot_p_r or e_dot_p_l:
            if e_dot_p_r > e_dot_p_l:
                vec_edge, e_dot_p = edge_right, e_dot_p_r
            else:
                vec_edge, e_dot_p = edge_left, e_dot_p_l

            vec_tan = (tan_avr - tan_avr.project(vec_edge)).normalized()
            # Make vec_tan perpendicular to vec_edge
            vec_up = vec_tan.cross(vec_edge)

            vec_width = vec_tan - (vec_tan.dot(vec_plane) / e_dot_p) * vec_edge
            vec_depth = vec_up - (vec_up.dot(vec_plane) / e_dot_p) * vec_edge
        else:
            vec_width = tan_avr
            vec_depth = norm_avr

        directions.append((vec_width, vec_depth))

    return verts, directions

def use_cashes(self, context):
    self.caches_valid = True

angle_presets = {'': 0,
                 '15°': radians(15),
                 '30°': radians(30),
                 '45°': radians(45),
                 '60°': radians(60),
                 '75°': radians(75),
                 '90°': radians(90),}
def assign_angle_presets(self, context):
    use_cashes(self, context)
    self.angle = angle_presets[self.angle_presets]

class OffsetEdges(bpy.types.Operator):
    """Offset Edges."""
    bl_idname = "mesh.offset_edges"
    bl_label = "Offset Edges"
    bl_options = {'REGISTER', 'UNDO'}

    geometry_mode: bpy.props.EnumProperty(
        items=[('offset', "Offset", "Offset edges"),
               ('extrude', "Extrude", "Extrude edges"),
               ('move', "Move", "Move selected edges")],
        name="Geometory mode", default='offset',
        update=use_cashes)
    width: bpy.props.FloatProperty(
        name="Width", default=.2, precision=4, step=1, update=use_cashes)
    flip_width: bpy.props.BoolProperty(
        name="Flip Width", default=False,
        description="Flip width direction", update=use_cashes)
    depth: bpy.props.FloatProperty(
        name="Depth", default=.0, precision=4, step=1, update=use_cashes)
    flip_depth: bpy.props.BoolProperty(
        name="Flip Depth", default=False,
        description="Flip depth direction", update=use_cashes)
    depth_mode: bpy.props.EnumProperty(
        items=[('angle', "Angle", "Angle"),
               ('depth', "Depth", "Depth")],
        name="Depth mode", default='angle', update=use_cashes)
    angle: bpy.props.FloatProperty(
        name="Angle", default=0, precision=3, step=.1,
        min=-2*pi, max=2*pi, subtype='ANGLE',
        description="Angle", update=use_cashes)
    flip_angle: bpy.props.BoolProperty(
        name="Flip Angle", default=False,
        description="Flip Angle", update=use_cashes)
    follow_face: bpy.props.BoolProperty(
        name="Follow Face", default=False,
        description="Offset along faces around")
    mirror_modifier: bpy.props.BoolProperty(
        name="Mirror Modifier", default=False,
        description="Take into account of Mirror modifier")
    edge_rail: bpy.props.BoolProperty(
        name="Edge Rail", default=False,
        description="Align vertices along inner edges")
    edge_rail_only_end: bpy.props.BoolProperty(
        name="Edge Rail Only End", default=False,
        description="Apply edge rail to end verts only")
    threshold: bpy.props.FloatProperty(
        name="Flat Face Threshold", default=radians(0.05), precision=5,
        step=1.0e-4, subtype='ANGLE',
        description="If difference of angle between two adjacent faces is "
                    "below this value, those faces are regarded as flat.",
        options={'HIDDEN'})
    caches_valid: bpy.props.BoolProperty(
        name="Caches Valid", default=False,
        options={'HIDDEN'})
    angle_presets: bpy.props.EnumProperty(
        items=[('', "", ""),
               ('15°', "15°", "15°"),
               ('30°', "30°", "30°"),
               ('45°', "45°", "45°"),
               ('60°', "60°", "60°"),
               ('75°', "75°", "75°"),
               ('90°', "90°", "90°"), ],
        name="Angle Presets", default='',
        update=assign_angle_presets)

    _cache_offset_infos = None
    _cache_edges_orig_ixs = None

    @classmethod
    def poll(self, context):
        return context.mode == 'EDIT_MESH'

    def draw(self, context):
        layout = self.layout
        layout.prop(self, 'geometry_mode', text="")
        #layout.prop(self, 'geometry_mode', expand=True)

        row = layout.row(align=True)
        row.prop(self, 'width')
        row.prop(self, 'flip_width', icon='ARROW_LEFTRIGHT', icon_only=True)

        layout.prop(self, 'depth_mode', expand=True)
        if self.depth_mode == 'angle':
            d_mode = 'angle'
            flip = 'flip_angle'
        else:
            d_mode = 'depth'
            flip = 'flip_depth'
        row = layout.row(align=True)
        row.prop(self, d_mode)
        row.prop(self, flip, icon='ARROW_LEFTRIGHT', icon_only=True)
        if self.depth_mode == 'angle':
            layout.prop(self, 'angle_presets', text="Presets", expand=True) 

        layout.separator()

        layout.prop(self, 'follow_face')

        row = layout.row()
        row.prop(self, 'edge_rail')
        if self.edge_rail:
            row.prop(self, 'edge_rail_only_end', text="OnlyEnd", toggle=True)

        layout.prop(self, 'mirror_modifier')

        #layout.operator('mesh.offset_edges', text='Repeat')

        if self.follow_face:
            layout.separator()
            layout.prop(self, 'threshold', text='Threshold')


    def get_offset_infos(self, bm, edit_object):
        if self.caches_valid and self._cache_offset_infos is not None:
            # Return None, indicating to use cache.
            return None, None

        time = perf_counter()

        set_edges_orig = collect_edges(bm)
        if set_edges_orig is None:
            self.report({'WARNING'},
                        "No edges selected.")
            return False, False

        if self.mirror_modifier:
            mirror_planes = collect_mirror_planes(edit_object)
            vert_mirror_pairs, set_edges = \
                get_vert_mirror_pairs(set_edges_orig, mirror_planes)

            if set_edges:
                set_edges_orig = set_edges
            else:
                #self.report({'WARNING'},
                #            "All selected edges are on mirror planes.")
                vert_mirror_pairs = None
        else:
            vert_mirror_pairs = None

        loops = collect_loops(set_edges_orig)
        if loops is None:
            self.report({'WARNING'},
                        "Overlap detected. Select non-overlap edge loops")
            return False, False

        vec_upward = (X_UP + Y_UP + Z_UP).normalized()
        # vec_upward is used to unify loop normals when follow_face is off.
        normal_fallback = Z_UP
        #normal_fallback = Vector(context.region_data.view_matrix[2][:3])
        # normal_fallback is used when loop normal cannot be calculated.

        follow_face = self.follow_face
        edge_rail = self.edge_rail
        er_only_end = self.edge_rail_only_end
        threshold = self.threshold

        offset_infos = []
        for lp in loops:
            verts, directions = get_directions(
                lp, vec_upward, normal_fallback, vert_mirror_pairs,
                follow_face=follow_face, edge_rail=edge_rail,
                edge_rail_only_end=er_only_end,
                threshold=threshold)
            if verts:
                offset_infos.append((verts, directions))

        # Saving caches.
        self._cache_offset_infos = _cache_offset_infos = []
        for verts, directions in offset_infos:
            v_ixs = tuple(v.index for v in verts)
            _cache_offset_infos.append((v_ixs, directions))
        self._cache_edges_orig_ixs = tuple(e.index for e in set_edges_orig)

        print("Preparing OffsetEdges: ", perf_counter() - time)

        return offset_infos, set_edges_orig

    def do_offset_and_free(self, bm, me, offset_infos=None, set_edges_orig=None):
        # If offset_infos is None, use caches.
        # Makes caches invalid after offset.

        #time = perf_counter()

        if offset_infos is None:
            # using cache
            bmverts = tuple(bm.verts)
            bmedges = tuple(bm.edges)
            edges_orig = [bmedges[ix] for ix in self._cache_edges_orig_ixs]
            verts_directions = []
            for ix_vs, directions in self._cache_offset_infos:
                verts = tuple(bmverts[ix] for ix in ix_vs)
                verts_directions.append((verts, directions))
        else:
            verts_directions = offset_infos
            edges_orig = list(set_edges_orig)

        if self.depth_mode == 'angle':
            w = self.width if not self.flip_width else -self.width
            angle = self.angle if not self.flip_angle else -self.angle
            width = w * cos(angle)
            depth = w * sin(angle)
        else:
            width = self.width if not self.flip_width else -self.width
            depth = self.depth if not self.flip_depth else -self.depth

        # Extrude
        if self.geometry_mode == 'move':
            geom_ex = None
        else:
            geom_ex = extrude_edges(bm, edges_orig)

        for verts, directions in verts_directions:
            move_verts(width, depth, verts, directions, geom_ex)

        clean(bm, self.geometry_mode, edges_orig, geom_ex)

        bpy.ops.object.mode_set(mode="OBJECT")
        bm.to_mesh(me)
        bpy.ops.object.mode_set(mode="EDIT")
        bm.free()
        self.caches_valid = False  # Make caches invalid.

        #print("OffsetEdges offset: ", perf_counter() - time)

    def execute(self, context):
        # In edit mode
        edit_object = context.edit_object
        bpy.ops.object.mode_set(mode="OBJECT")

        me = edit_object.data
        bm = bmesh.new()
        bm.from_mesh(me)

        offset_infos, edges_orig = self.get_offset_infos(bm, edit_object)
        if offset_infos is False:
            bpy.ops.object.mode_set(mode="EDIT")
            return {'CANCELLED'}

        self.do_offset_and_free(bm, me, offset_infos, edges_orig)

        return {'FINISHED'}

    def restore_original_and_free(self, context):
        self.caches_valid = False  # Make caches invalid.
        context.area.header_text_set()

        me = context.edit_object.data
        bpy.ops.object.mode_set(mode="OBJECT")
        self._bm_orig.to_mesh(me)
        bpy.ops.object.mode_set(mode="EDIT")

        self._bm_orig.free()
        context.area.header_text_set()

    def invoke(self, context, event):
        # In edit mode
        edit_object = context.edit_object
        me = edit_object.data
        bpy.ops.object.mode_set(mode="OBJECT")
        for p in me.polygons:
            if p.select:
                self.follow_face = True
                break

        self.caches_valid = False
        bpy.ops.object.mode_set(mode="EDIT")
        return self.execute(context)

class OffsetEdgesMenu(bpy.types.Menu):
    bl_idname = "VIEW3D_MT_edit_mesh_offset_edges"
    bl_label = "Offset Edges"

    def draw(self, context):
        layout = self.layout
        layout.operator_context = 'INVOKE_DEFAULT'

        off = layout.operator('mesh.offset_edges', text='Offset')
        off.geometry_mode = 'offset'

        ext = layout.operator('mesh.offset_edges', text='Extrude')
        ext.geometry_mode = 'extrude'

        mov = layout.operator('mesh.offset_edges', text='Move')
        mov.geometry_mode = 'move'

classes = (
OffsetEdges,
OffsetEdgesMenu,
)		

def draw_item(self, context):
    self.layout.menu("VIEW3D_MT_edit_mesh_offset_edges")


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.VIEW3D_MT_edit_mesh_edges.prepend(draw_item)


def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    bpy.types.VIEW3D_MT_edit_mesh_edges.remove(draw_item)


if __name__ == '__main__':
    register()