diff options
Diffstat (limited to 'mesh_tools/mesh_offset_edges.py')
-rw-r--r-- | mesh_tools/mesh_offset_edges.py | 791 |
1 files changed, 791 insertions, 0 deletions
diff --git a/mesh_tools/mesh_offset_edges.py b/mesh_tools/mesh_offset_edges.py new file mode 100644 index 00000000..524076a5 --- /dev/null +++ b/mesh_tools/mesh_offset_edges.py @@ -0,0 +1,791 @@ +# ***** 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°': 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=[('0°', "0°", "0°"), + ('15°', "15°", "15°"), + ('30°', "30°", "30°"), + ('45°', "45°", "45°"), + ('60°', "60°", "60°"), + ('75°', "75°", "75°"), + ('90°', "90°", "90°"), ], + name="Angle Presets", default='0°', + 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() |