From 0b2d51126c5fbeebb313ebd791fbbbf79b927f8d Mon Sep 17 00:00:00 2001 From: Howard Trickey Date: Mon, 18 Feb 2019 20:50:21 -0500 Subject: Update mesh_inset (alternate inset using straight skeleton) for 2.8. Update also made it modal, with interaction mode as in built-in inset, where mouse movement adjusts amount and holding control makes mouse movement affext height. Also renamed from "Inset Polygon" to "Inset Straight Skeleton" to lessen confusion with built-in one and emphasizing why this one is different. Recommend binding mesh.insetstraightskeleton to a key, like Alt-i --- mesh_inset/__init__.py | 225 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 175 insertions(+), 50 deletions(-) (limited to 'mesh_inset') diff --git a/mesh_inset/__init__.py b/mesh_inset/__init__.py index e7c7d587..781b6962 100644 --- a/mesh_inset/__init__.py +++ b/mesh_inset/__init__.py @@ -19,12 +19,12 @@ # bl_info = { - "name": "Inset Polygon", + "name": "Inset Straight Skeleton", "author": "Howard Trickey", - "version": (1, 0, 1), - "blender": (2, 73, 0), - "location": "View3D > Tools", - "description": "Make an inset polygon inside selection.", + "version": (1, 1), + "blender": (2, 80, 0), + "location": "3DView Operator", + "description": "Make an inset inside selection using straight skeleton algorithm.", "warning": "", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" "Scripts/Modeling/Inset-Polygon", @@ -45,22 +45,35 @@ import math import bpy import bmesh import mathutils +from mathutils import Vector +from bpy_extras import view3d_utils +import gpu +from gpu_extras.batch import batch_for_shader + from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, ) +SpaceView3D = bpy.types.SpaceView3D + +INSET_VALUE = 0 +HEIGHT_VALUE = 1 +NUM_VALUES = 2 + +# TODO: make a dooted-line shader +shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') -class Inset(bpy.types.Operator): - bl_idname = "mesh.insetpoly" - bl_label = "Inset Polygon" - bl_description = "Make an inset polygon inside selection" - bl_options = {'REGISTER', 'UNDO'} +class MESH_OT_InsetStraightSkeleton(bpy.types.Operator): + bl_idname = "mesh.insetstraightskeleton" + bl_label = "Inset Straight Skeleton" + bl_description = "Make an inset inside selection using straight skeleton algorithm" + bl_options = {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'} inset_amount: FloatProperty(name="Amount", description="Amount to move inset edges", - default=5.0, + default=0.0, min=0.0, max=1000.0, soft_min=0.0, @@ -77,15 +90,9 @@ class Inset(bpy.types.Operator): region: BoolProperty(name="Region", description="Inset selection as one region?", default=True) - scale: EnumProperty(name="Scale", - description="Scale for amount", - items=[ - ('PERCENT', "Percent", - "Percentage of maximum inset amount"), - ('ABSOLUTE', "Absolute", - "Length in blender units") - ], - default='PERCENT') + quadrangulate: BoolProperty(name="Quadrangulate", + description="Quadrangulate after inset?", + default=True) @classmethod def poll(cls, context): @@ -96,32 +103,152 @@ class Inset(bpy.types.Operator): layout = self.layout box = layout.box() box.label(text="Inset Options:") - box.prop(self, "scale") box.prop(self, "inset_amount") box.prop(self, "inset_height") box.prop(self, "region") + box.prop(self, "quadrangulate") def invoke(self, context, event): + self.modal = True + # make backup bmesh from current mesh, after flushing editmode to mesh + bpy.context.object.update_from_editmode() + self.backup = bmesh.new() + self.backup.from_mesh(bpy.context.object.data) + self.inset_amount = 0.0 + self.inset_height = 0.0 + self.center, self.center3d = calc_select_center(context) + self.center_pixel_size = calc_pixel_size(context, self.center3d) + udpi = context.preferences.system.dpi + upixelsize = context.preferences.system.pixel_size + self.pixels_per_inch = udpi * upixelsize + self.value_mode = INSET_VALUE + self.initial_length = [-1.0, -1.0] + self.scale = [self.center_pixel_size] * NUM_VALUES + self.calc_initial_length(event, True) + self.mouse_cur = Vector((event.mouse_region_x, event.mouse_region_y)) + col = context.preferences.themes["Default"].view_3d.view_overlay + self.line_color = (col.r, col.g, col.b, 1.0) + self.action(context) - return {'FINISHED'} + + context.window_manager.modal_handler_add(self) + self.draw_handle = SpaceView3D.draw_handler_add(draw_callback, + (self,), 'WINDOW', 'POST_PIXEL') + + return {'RUNNING_MODAL'} + + def calc_initial_length(self, event, mode_changed): + mdiff = self.center - Vector((event.mouse_region_x, event.mouse_region_y)) + mlen = mdiff.length; + vmode = self.value_mode + if mode_changed or self.initial_length[vmode] == -1: + if vmode == INSET_VALUE: + value = self.inset_amount + else: + value = self.inset_height + sc = self.scale[vmode] + if value != 0.0: + mlen = mlen - value / sc + self.initial_length[vmode] = mlen + + def modal(self, context, event): + if event.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']: + if self.modal: + self.backup.free() + if self.draw_handle: + SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW') + context.area.tag_redraw() + if event.type == 'LEFTMOUSE': # Confirm + return {'FINISHED'} + else: # Cancel + return {'CANCELLED'} + else: + # restore mesh to original state + bpy.ops.object.editmode_toggle() + self.backup.to_mesh(bpy.context.object.data) + bpy.ops.object.editmode_toggle() + if event.type == 'MOUSEMOVE': + if self.value_mode == INSET_VALUE and event.ctrl: + self.value_mode = HEIGHT_VALUE + self.calc_initial_length(event, True) + elif self.value_mode == HEIGHT_VALUE and not event.ctrl: + self.value_mode = INSET_VALUE + self.calc_initial_length(event, True) + self.mouse_cur = Vector((event.mouse_region_x, event.mouse_region_y)) + vmode = self.value_mode + mdiff = self.center - self.mouse_cur + value = (mdiff.length - self.initial_length[vmode]) * self.scale[vmode] + if vmode == INSET_VALUE: + self.inset_amount = value + else: + self.inset_height = value + elif event.type == 'R' and event.value == 'PRESS': + self.region = not self.region + elif event.type == 'Q' and event.value == 'PRESS': + self.quadrangulate = not self.quadrangulate + self.action(context) + + return {'RUNNING_MODAL'} def execute(self, context): + self.modal = False self.action(context) return {'FINISHED'} def action(self, context): - save_global_undo = bpy.context.preferences.edit.use_global_undo - bpy.context.preferences.edit.use_global_undo = False obj = bpy.context.active_object mesh = obj.data do_inset(mesh, self.inset_amount, self.inset_height, self.region, - self.scale == 'PERCENT') - bpy.context.preferences.edit.use_global_undo = save_global_undo + self.quadrangulate) bpy.ops.object.editmode_toggle() bpy.ops.object.editmode_toggle() -def do_inset(mesh, amount, height, region, as_percent): +def draw_callback(op): + startpos = op.mouse_cur + endpos = op.center + coords = [startpos.to_tuple(), endpos.to_tuple()] + batch = batch_for_shader(shader, 'LINES', {"pos": coords}) + + try: + shader.bind() + shader.uniform_float("color", op.line_color) + batch.draw(shader) + except: + pass + +def calc_pixel_size(context, co): + # returns size in blender units of a pixel at 3d coord co + # see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat + m = context.region_data.perspective_matrix + v1 = m[0].to_3d() + v2 = m[1].to_3d() + ll = min(v1.length_squared, v2.length_squared) + len_pz = 2.0 / math.sqrt(ll) + len_sz = max(context.region.width, context.region.height) + rv3dpixsize = len_pz / len_sz + proj = m[3][0] * co[0] + m[3][1] * co[1] + m[3][2] * co[2] + m[3][3] + ups = context.preferences.system.pixel_size + return proj * rv3dpixsize * ups + +def calc_select_center(context): + # returns region 2d coord and global 3d coord of selection center + ob = bpy.context.active_object + mesh = ob.data + center = Vector((0.0, 0.0, 0.0)) + n = 0 + for v in mesh.vertices: + if v.select: + center = center + Vector(v.co) + n += 1 + if n > 0: + center = center / n + world_center = ob.matrix_world @ center + world_center_2d = view3d_utils.location_3d_to_region_2d( \ + context.region, context.region_data, world_center) + return (world_center_2d, world_center) + +def do_inset(mesh, amount, height, region, quadrangulate): if amount <= 0.0: return pitch = math.atan(height / amount) @@ -142,7 +269,7 @@ def do_inset(mesh, amount, height, region, as_percent): m.face_data.append(f.index) orig_numv = len(m.points.pos) orig_numf = len(m.faces) - model.BevelSelectionInModel(m, amount, pitch, True, region, as_percent) + model.BevelSelectionInModel(m, amount, pitch, quadrangulate, region, False) if len(m.faces) == orig_numf: # something went wrong with Bevel - just treat as no-op return @@ -160,41 +287,39 @@ def do_inset(mesh, amount, height, region, as_percent): continue # copy face attributes from old face that it was derived from bfi = blender_old_face_index[i] - if bfi and 0 <= bfi < start_faces: - bm.faces.ensure_lookup_table() - oldface = bm.faces[bfi] - bfacenew = bm.faces.new(vs, oldface) - # bfacenew.copy_from_face_interp(oldface) - else: - bfacenew = bm.faces.new(vs) - new_faces.append(bfacenew) + # sometimes, not sure why, this face already exists + # bmesh will give a value error in bm.faces.new() in that case + try: + if bfi and 0 <= bfi < start_faces: + bm.faces.ensure_lookup_table() + oldface = bm.faces[bfi] + bfacenew = bm.faces.new(vs, oldface) + # bfacenew.copy_from_face_interp(oldface) + else: + bfacenew = bm.faces.new(vs) + new_faces.append(bfacenew) + except ValueError: + # print("dup face with amount", amount) + + # print([v.index for v in vs]) + pass # deselect original faces for face in selfaces: face.select_set(False) # remove original faces - bmesh.ops.delete(bm, geom=selfaces, context=5) # 5 = DEL_FACES + bmesh.ops.delete(bm, geom=selfaces, context='FACES') # select all new faces (should only select inner faces, but that needs more surgery on rest of code) for face in new_faces: face.select_set(True) + bmesh.update_edit_mesh(mesh) def remove_dups(vs): seen = set() return [x for x in vs if not (x in seen or seen.add(x))] -def panel_func(self, context): - self.layout.label(text="Inset Polygon:") - self.layout.operator("mesh.insetpoly", text="Inset Polygon") - def register(): - bpy.utils.register_class(Inset) - bpy.types.VIEW3D_PT_tools_meshedit.append(panel_func) - + bpy.utils.register_class(MESH_OT_InsetStraightSkeleton) def unregister(): - bpy.utils.unregister_class(Inset) - bpy.types.VIEW3D_PT_tools_meshedit.remove(panel_func) - - -if __name__ == "__main__": - register() + bpy.utils.unregister_class(MESH_OT_InsetStraightSkeleton) -- cgit v1.2.3