OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modifiers / smooth.py

446 lines · python ref:
import bpy, bmesh
from ... utility import addon
from ... utility import modifier
from ... utility.base_modal_controls import Base_Modal_Controls
from ... ui_framework.master import Master
from ... ui_framework.utils.mods_list import get_mods_list
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utils.modal_frame_drawing import draw_modal_frame
from ... utils.cursor_warp import mouse_warp
from ... utility import method_handler


class HOPS_OT_MOD_Smooth(bpy.types.Operator):
    bl_idname = 'hops.mod_smooth'
    bl_label = 'Adjust Smooth Modifier'
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING', 'GRAB_CURSOR'}
    bl_description = '''LMB - Adjust Smooth Modifier
LMB + Ctrl - Create new Smooth Modifier
LMB + Shift - Auto Vertex Group
LMB + Alt - Use Laplacian Smooth

Press H for help

'''


    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return obj and obj.type == 'MESH' and obj.mode in {'OBJECT', 'EDIT'}


    def invoke(self, context, event):
        self.mod_keys = ('iterations', 'use_x', 'use_y', 'use_z', 'vertex_group', 'invert_vertex_group', 'show_viewport', 'show_in_editmode')

        self.create_new = event.ctrl
        self.auto_vgroup = event.shift
        self.start_laplacian = event.alt
        self.modal_scale = addon.preference().ui.Hops_modal_scale
        self.obj = context.active_object
        self.mods = [m for m in self.obj.modifiers if m.type in {'SMOOTH', 'LAPLACIANSMOOTH'}]

        if not self.mods:
            self.create_new = True

        if self.create_new:
            self.mod = self.obj.modifiers.new('Smooth', 'SMOOTH')
            self.mods.append(self.mod)
        else:
            self.mod = self.mods[-1]

        self.clean_vertex_groups()

        self.values = {m: {} for m in self.mods}
        for mod in self.mods:
            self.store(mod)

        if self.auto_vgroup:
            self.create_vgroup(self.mod)
        elif self.obj.mode == 'EDIT':
            self.create_editmode_vgroup(self.mod)

        if self.create_new and self.start_laplacian:
            self.mod = self.switch_type(self.mod)

        self.factor_buffer = self.get_factor(self.mod)
        self.setup_vertex_group_buffer()

        for mod in self.mods:
            mod.show_viewport = True
            mod.show_in_editmode = True

        self.master = Master(context=context)
        self.master.only_use_fast_ui = True
        self.base_controls = Base_Modal_Controls(context, event)
        self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
        self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_shader, (context,), 'WINDOW', 'POST_PIXEL')

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


    def modal(self, context, event):
        self.master.receive_event(event=event)
        self.base_controls.update(context, event)
        mouse_warp(context, event)

        if self.base_controls.pass_through:
            return {'PASS_THROUGH'}

        elif event.type == 'Z' and (event.shift or event.alt):
            return {'PASS_THROUGH'}

        elif self.base_controls.confirm:
            context.area.header_text_set(text=None)
            self.report({'INFO'}, 'Finished')
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            return {'FINISHED'}

        elif self.base_controls.cancel:
            context.area.header_text_set(text=None)
            self.report({'INFO'}, 'Cancelled')
            self.cancel(context)
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            return {'CANCELLED'}

        elif self.base_controls.scroll:
            if event.shift:
                if self.base_controls.scroll == 1:
                    bpy.ops.object.modifier_move_up(modifier=self.mod.name)
                else:
                    bpy.ops.object.modifier_move_down(modifier=self.mod.name)

            elif event.ctrl:
                self.scroll(self.base_controls.scroll)

            elif event.alt and self.mod.vertex_group:
                scroll = self.base_controls.scroll
                groups = self.obj.vertex_groups
                group = self.vertex_group_buffer

                index = groups.get(group).index
                index = (index + scroll + len(groups)) % len(groups)

                self.vertex_group_buffer = groups[index].name
                self.mod.vertex_group = self.vertex_group_buffer

            else:
                self.mod.iterations += self.base_controls.scroll
                self.mod.iterations = max(self.mod.iterations, 0)

            self.factor_buffer = self.get_factor(self.mod)

        elif event.type == 'MOUSEMOVE':
            self.factor_buffer += self.base_controls.mouse
            self.factor_buffer = max(self.factor_buffer, 0)
            digits = 2 if event.ctrl and event.shift else 1 if event.ctrl else 3
            self.set_factor(self.mod,  round(self.factor_buffer, digits))

        elif event.type in ('X', 'Y', 'Z') and event.ctrl:
            if event.type == 'X' and event.value == 'PRESS':
                self.mod.use_x = not self.mod.use_x

            elif event.type == 'Y' and event.value == 'PRESS':
                self.mod.use_y = not self.mod.use_y

            elif event.type == 'Z' and event.value == 'PRESS':
                self.mod.use_z = not self.mod.use_z

        elif event.type in ('X', 'Y', 'Z'):
            if event.type == 'X' and event.value == 'PRESS':
                self.mod.use_x = True
                self.mod.use_y = False
                self.mod.use_z = False

            elif event.type == 'Y' and event.value == 'PRESS':
                self.mod.use_x = False
                self.mod.use_y = True
                self.mod.use_z = False

            elif event.type == 'Z' and event.value == 'PRESS':
                self.mod.use_x = False
                self.mod.use_y = False
                self.mod.use_z = True

        elif event.type == 'Q' and event.value == 'PRESS':
            bpy.ops.object.modifier_move_up(modifier=self.mod.name)

        elif event.type == 'W' and event.value == 'PRESS':
            bpy.ops.object.modifier_move_down(modifier=self.mod.name)

        elif event.type == 'T' and event.value == 'PRESS':
            self.mod = self.switch_type(self.mod)

        elif event.type == 'I' and event.value == 'PRESS' and self.mod.vertex_group:
            self.mod.invert_vertex_group = not self.mod.invert_vertex_group
            action = 'Enabled' if self.mod.invert_vertex_group else 'Disabled'
            bpy.ops.hops.display_notification(info=f'Smooth - {action} Invert Vertex Group')

        elif event.type == 'V' and event.value == 'PRESS':
            if self.mod.vertex_group:
                self.mod.vertex_group = ''
                bpy.ops.hops.display_notification(info=f'Smooth - Disabled Vertex Group')

            elif self.vertex_group_buffer:
                self.mod.vertex_group = self.vertex_group_buffer
                bpy.ops.hops.display_notification(info=f'Smooth - Enabled Vertex Group')

        self.draw_master(context=context)
        context.area.tag_redraw()
        return {'RUNNING_MODAL'}


    def cancel(self, context):
        for mod in self.mods:
            self.reset(mod)

        if self.create_new:
            self.obj.modifiers.remove(self.mods[-1])


    def scroll(self, direction):
        index = self.mods.index(self.mod)
        index = (index + direction + len(self.mods)) % len(self.mods)
        self.mod = self.mods[index]

        self.factor_buffer = self.get_factor(self.mod)
        self.setup_vertex_group_buffer()


    def get_factor(self, mod):
        if mod.type == 'SMOOTH':
            return mod.factor
        else:
            return mod.lambda_factor


    def set_factor(self, mod, value):
        if mod.type == 'SMOOTH':
            mod.factor = value
        else:
            mod.lambda_factor = value


    def store(self, mod):
        self.values[mod]['type'] = mod.type
        self.values[mod]['factor'] = self.get_factor(mod)

        for key in self.mod_keys:
            self.values[mod][key] = getattr(mod, key)


    def reset(self, mod):
        if self.values[mod]['type'] != mod.type:
            mod = self.switch_type(mod)

        self.set_factor(mod, self.values[mod]['factor'])

        for key in self.mod_keys:
            setattr(mod, key, self.values[mod][key])


    def switch_type(self, mod):
        kind = 'Laplacian Smooth' if mod.type == 'SMOOTH' else 'Smooth'
        bpy.ops.hops.display_notification(info=f'Smooth - Switched to {kind}')

        if mod.type == 'SMOOTH':
            new = self.obj.modifiers.new('Laplacian Smooth', 'LAPLACIANSMOOTH')
        else:
            new = self.obj.modifiers.new('Smooth', 'SMOOTH')

        self.set_factor(new, self.get_factor(mod))

        for key in self.mod_keys:
            value = getattr(mod, key)
            setattr(new, key, value)

        for index, modifier in enumerate(self.obj.modifiers):
            if modifier == mod:
                for i in range(len(self.obj.modifiers) - (index + 1)):
                    bpy.ops.object.modifier_move_up(modifier=new.name)
                break

        self.values[new] = self.values[mod]
        self.values.pop(mod)

        self.mods.insert(self.mods.index(mod), new)
        self.mods.remove(mod)

        self.obj.modifiers.remove(mod)
        return new


    def clean_vertex_groups(self):
        for mod in self.mods:
            if not self.obj.vertex_groups.get(mod.vertex_group):
                mod.vertex_group = ''


    def setup_vertex_group_buffer(self):
        if self.mod.vertex_group:
            self.vertex_group_buffer = self.mod.vertex_group
        elif self.obj.vertex_groups:
            self.vertex_group_buffer = self.obj.vertex_groups[0].name
        else:
            self.vertex_group_buffer = ''


    def create_vgroup(self, mod):
        original_mode = self.obj.mode

        if original_mode == 'EDIT':
            bpy.ops.object.mode_set(mode='OBJECT')

        vertex_group = None

        for group in self.obj.vertex_groups:
            if group.name == 'HOPS_Smooth':
                vertex_group = group

        if not vertex_group:
            vertex_group = self.obj.vertex_groups.new(name='HOPS_Smooth')

        verts = list(range(len(self.obj.data.vertices)))
        vertex_group.add(index=verts, weight=1.0, type='REPLACE')

        bm = bmesh.new()
        bm.from_mesh(self.obj.data)

        if bpy.app.version[0] >= 4:
            bevel = bm.edges.layers.float.get('bevel_weight_edge')
            if bevel is None:
                bevel = bm.edges.layers.float.new('bevel_weight_edge')
        else:
            bevel = bm.edges.layers.bevel_weight.verify()

        if bpy.app.version[0] >= 4:
            crease = bm.edges.layers.float.get('crease_edge')
            if crease is None:
                crease = bm.edges.layers.float.new('crease_edge')
        else:
            crease = bm.edges.layers.crease.verify()

        verts = []
        for v in bm.verts:
            if v.is_boundary:
                verts.append(v.index)
                continue

            for e in v.link_edges:
                if e.seam or not e.smooth or e[bevel] != 0.0 or e[crease] != 0.0:
                    verts.append(v.index)
                    continue

        vertex_group.remove(index=verts)
        bpy.ops.hops.display_notification(info='Smooth - Auto Vertex Group')
        mod.vertex_group = vertex_group.name
        bm.free()

        if original_mode == 'EDIT':
            bpy.ops.object.mode_set(mode='EDIT')


    def create_editmode_vgroup(self, mod):
        self.obj.update_from_editmode()
        bm = bmesh.from_edit_mesh(self.obj.data)
        verts = [v.index for v in bm.verts if v.select]

        if not verts:
            return

        bpy.ops.object.mode_set(mode='OBJECT')
        vertex_group = self.obj.vertex_groups.new(name='HOPS_Edit_Smooth')
        vertex_group.add(index=verts, weight=1.0, type='ADD')
        bpy.ops.object.mode_set(mode='EDIT')

        bpy.ops.hops.display_notification(info='Smooth - Selected Vertex Group')
        mod.vertex_group = vertex_group.name


    def draw_master(self, context):
        self.master.setup()

        if self.master.should_build_fast_ui():

            # Main
            win_list = []
            axes = []
            if self.mod.use_x:
                axes.append('X')
            if self.mod.use_y:
                axes.append('Y')
            if self.mod.use_z:
                axes.append('Z')
            axes = ', '.join(axes)

            if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1: #Fast Floating
                win_list.append(f'{self.mod.iterations}')
                win_list.append(f'{self.get_factor(self.mod):.3f}')
                win_list.append(f'{axes}')
            else:
                win_list.append('Smooth')
                win_list.append(f'Iterations: {self.mod.iterations}')
                win_list.append(f'Factor: {self.get_factor(self.mod):.3f}')
                win_list.append(f'Axis: {axes}')
                if self.mod.vertex_group:
                    win_list.append(f'VGroup: {self.mod.vertex_group}')
                    win_list.append(f'Invert: {self.mod.invert_vertex_group}')

            # Help
            help_items = {"GLOBAL" : [], "STANDARD" : []}

            help_items["GLOBAL"] = [
                ("M", "Toggle mods list"),
                ("H", "Toggle help"),
                ("~", "Toggle UI Display Type"),
                ("O", "Toggle viewport rendering")]

            help_items["STANDARD"] = [
                ('Scroll',         'Set iterations'),
                ('X, Y, Z',        'Set axis'),
                ('Ctrl + X, Y, Z', 'Toggle axis'),
                ('Q',              'Move mod DOWN'),
                ('W',              'Move mod UP'),
                ('T',              'Switch mod type'),
                ('V',              'Toggle use vertex group')]

            h_append = help_items["STANDARD"].append
            
            if self.mod.vertex_group:
                h_append(['I',              'Toggle invert vertex group'])
                h_append(['Alt + Scroll',   'Cycle vertex groups'])

            h_append(['Shift + Scroll', 'Move mod up/down'])
            h_append(['Ctrl + Scroll',  'Cycle smooth mods'])

            # Mods
            active_mod = ""
            if self.mod != None:
                active_mod = self.mod.name
            mods_list = get_mods_list(mods=bpy.context.active_object.modifiers)

            self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image='Demote', mods_list=mods_list, active_mod_name=active_mod)

        self.master.finished()


    def safe_draw_shader(self, context):
        method_handler(self.draw_shader,
            arguments = (context,),
            identifier = 'UI Framework',
            exit_method = self.remove_shader)


    def remove_shader(self):
        if self.draw_handle:
            self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')


    def draw_shader(self, context):
        draw_modal_frame(context)