OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / adjust_auto_smooth.py

500 lines · python ref:
import bpy, math, numpy
from bpy.props import BoolProperty
from math import radians, degrees
from . import infobar
from ... utility import addon, geometry_nodes, object
from ... ui_framework.master import Master
from ... utility import ops
from ... utility.base_modal_controls import Base_Modal_Controls
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 ... utils.objects import set_bool_tagets_on_objects_to_smooth
from ... utility import method_handler


DESC = """Interactive Autosmooth Adjustment

LMB   - Adjust autosmoothing
CTRL  - Start at 60°
SHIFT - Start at 30°
ALT   - Start at 15°

Press H for help
"""


class HOPS_OT_AdjustAutoSmooth(bpy.types.Operator):
    bl_idname = 'hops.adjust_auto_smooth'
    bl_label = 'Adjust Auto Smooth'
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
    bl_description = DESC

    angle: bpy.props.FloatProperty(
        name='Auto Smooth Angle',
        description='The angle at which to automatically sharpen edges',
        default=35,
        min=0,
        max=180,
    )

    flag: BoolProperty(
        name = 'Use Bevel Special Behavior',
        default = False,
        description = 'Ignore Ctrl keypress')

    @classmethod
    def poll(cls, context):
        return any(o.type == 'MESH' for o in context.selected_objects)


    def invoke(self, context, event):

        self.number_to_angle = {
            'ONE': 15,
            'TWO': 20,
            'THREE': 30,
            'FOUR': 45,
            'FIVE': 60,
            'SIX': 75,
            'SEVEN': 90,
            'EIGHT': 180,
        }

        ops.shade_smooth()
        self.objects = [o for o in context.selected_objects if o.type == 'MESH']
        self.settings = {o: {} for o in self.objects}

        # if event.ctrl:
        #     self.angle = 60
        # if event.shift:
        #     self.angle = 30
        # if event.alt:
        #     self.angle = 15

        for obj in self.objects:
            self.settings[obj]['use_auto_smooth'] = obj.data.use_auto_smooth
            obj.data.use_auto_smooth = True

            self.settings[obj]['auto_smooth_angle'] = obj.data.auto_smooth_angle
            obj.data.auto_smooth_angle = math.radians(self.angle)

            bevmods = bevels(obj, angle=True)

            if self.flag:
                if bevmods:
                    for mod in bevmods:
                        mod.show_viewport = False

        self.modal_scale = addon.preference().ui.Hops_modal_scale
        self.buffer = self.angle

        # Base Systems
        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.area.header_text_set(text=f'Auto Smooth Angle: {self.angle:.1f}')
        context.window_manager.modal_handler_add(self)
        infobar.initiate(self)
        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        # Base Systems
        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'}

        offset = self.base_controls.mouse * 10

        if event.type == 'MOUSEMOVE':
            self.buffer = min(max(self.buffer + offset, 0), 180)
            self.angle = round(self.buffer, 0 if event.ctrl else 1)
            self.update(context)

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

        elif event.type in {'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT'} and event.value == 'PRESS':
            self.buffer = self.number_to_angle[event.type]
            self.angle = self.number_to_angle[event.type]
            self.update(context)

        elif self.base_controls.scroll:
            self.buffer += 5 * self.base_controls.scroll
            self.angle += 5 * self.base_controls.scroll
            self.update(context)

        elif self.base_controls.cancel:
            self.cancel(context)

            obj = bpy.context.object
            for mod in obj.modifiers:
                if mod.type == 'BEVEL': #and mod.limit_method == 'ANGLE':
                    mod.show_viewport = True

            context.area.header_text_set(text=None)
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            infobar.remove(self)
            return {'CANCELLED'}

        elif self.base_controls.confirm:
            obj = bpy.context.object

            if self.flag :
                for mod in obj.modifiers:
                    if mod.type == 'BEVEL': # and mod.limit_method == 'ANGLE':
                        mod.show_viewport = True
                self.angle = 60
                self.buffer = 60
                bpy.context.object.data.auto_smooth_angle = radians(60)
                self.flag = False
            else:
                for mod in obj.modifiers:
                    if mod.type == 'BEVEL': #and mod.limit_method == 'ANGLE':
                        mod.show_viewport = True
            context.area.header_text_set(text=None)
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            infobar.remove(self)
            return {'FINISHED'}

        elif event.type == 'G' and event.value == 'PRESS':
            bpy.context.object.hops.is_global = not bpy.context.object.hops.is_global

        elif event.type == 'B' and event.value == 'PRESS':
            obj = bpy.context.object

            for mod in obj.modifiers:
                if mod.type == 'BEVEL': #and mod.limit_method == 'ANGLE':
                    mod.show_viewport = not mod.show_viewport

        elif event.type == "A" and event.value == 'PRESS' and not event.shift and not event.alt:
            mod = None
            obj = bpy.context.object

            bevmods = bevels(obj, angle=True)

            if bevmods:
                for mod in bevmods:
                    mod.show_viewport = True
                    mod.angle_limit = radians(self.angle)

                    self.report({'INFO'}, F'AngleFound')
                    self.update(context)
                #_____________
                self.angle = 60
                self.buffer = 60
                context.area.header_text_set(text=None)
                self.finished = True
                self.remove_shader()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                self.master.run_fade()
                bpy.ops.hops.adjust_bevel('INVOKE_DEFAULT')
                return {"FINISHED"}
            else:
                context.area.header_text_set(text=None)
                self.finished = True
                self.remove_shader()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                self.master.run_fade()
                #_____________
                #mod.show_viewport = True
                if not bevmods:
                    add_bevel_modifier(context, obj, radians(self.angle))
                bpy.ops.hops.adjust_bevel('INVOKE_DEFAULT')
                bpy.context.object.data.auto_smooth_angle = radians(60)
                self.report({'INFO'}, F'Bevel Added')
                return {"FINISHED"}
            self.update(context)

        elif event.type == "A" and event.value == 'PRESS' and event.shift:
            bpy.context.object.data.use_auto_smooth = not bpy.context.object.data.use_auto_smooth
            self.report({'INFO'}, F'Autosmooth {bpy.context.object.data.use_auto_smooth}')

        elif event.type == 'S' and event.value == 'PRESS':
            if event.shift:
                bpy.ops.hops.sharpen(behavior='SSHARP', mode='SSHARP', additive_mode=True, auto_smooth_angle=radians(self.angle), is_global=bpy.context.object.hops.is_global)
                self.report({'INFO'}, F'Sharpen - Exit')
                self.remove_shader()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                self.master.run_fade()
                infobar.remove(self)
                return {'FINISHED'}

            else:
                smooth_cutters_rec(self.objects)
                bpy.ops.hops.display_notification(info=F'Boolshapes - Set Smooth')

        elif event.type == 'R' and event.value == 'PRESS':
            bpy.ops.hops.sharpen(behavior='RESHARP', mode='SSHARP', additive_mode=False, auto_smooth_angle=radians(self.angle), is_global=bpy.context.object.hops.is_global)
            self.report({'INFO'}, F'Resharpen - Exit')
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            infobar.remove(self)
            return {'FINISHED'}

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


    def update(self, context):
        angle = math.radians(self.angle)

        context.area.header_text_set(text=f'Auto Smooth Angle: {self.angle:.1f}')

        for obj in self.objects:
            obj.data.auto_smooth_angle = angle


    def cancel(self, context):
        for obj in self.objects:
            # obj.data.use_auto_smooth = self.settings[obj]['use_auto_smooth'] # Commented out because mx prefers it this way
            obj.data.auto_smooth_angle = self.settings[obj]['auto_smooth_angle']


    def finish(self, context):
        context.area.header_text_set(text=None)
        self.remove_ui()
        infobar.remove(self)
        return {"FINISHED"}


    def draw_master(self, context):
        self.master.setup()
        if not self.master.should_build_fast_ui(): return

        obj = bpy.context.object

        # Main
        win_list = []
        if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1: #fast
            win_list.append(f'{self.angle:.1f}')
            win_list.append(f'{bpy.context.object.hops.is_global}')
            win_list.append(f'{bpy.context.object.data.use_auto_smooth}')
        else:
            if self.flag:
                win_list.append('Auto Smooth Scan') #not fast
            else:
                win_list.append('Auto Smooth') #not fast
            win_list.append(f'{self.angle:.1f}')
            win_list.append(f'Global Sharpen :{bpy.context.object.hops.is_global}')
            if not obj.modifiers:
                win_list.append('[A] Add Bevel')
            if bpy.context.object.data.use_auto_smooth == False:
                win_list.append('Autosmooth OFF')
            if self.flag:
                win_list.append('[A] Return / [B] Toggle Bevel')
                win_list.append('[LMB] Apply')

        # 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"] = [
            ('8',          'Set angle to 180°'),
            ('7',          'Set angle to 90°'),
            ('6',          'Set angle to 75°'),
            ('5',          'Set angle to 60°'),
            ('4',          'Set angle to 45°'),
            ('3',          'Set angle to 30°'),
            ('2',          'Set angle to 20°'),
            ('1',          'Set angle to 15°'),
            ('Shift S',    f'Sharpen {self.angle:.1f}° - Exit'),
            ('S',          'Set Booleans to smooth'),
            ('R',          f'Resharpen - Exit'),
            ('G',          f'Toggle Global : {bpy.context.object.hops.is_global}'),
            ('Shift + A',  f'Autosmooth Toggle {bpy.context.object.data.use_auto_smooth}'),
            ('Scroll   ',  'Increment angle by 5°'),
            ('Mouse    ',  f'Adjust angle smoothly: {self.angle:.1f}'),
            ('A',          'Transfer Autosmooth to Bevel - Exit')]

        # Mods
        mods_list = []
        for mod in reversed(context.active_object.modifiers):
            mods_list.append([mod.name, str(mod.type)])

        self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image='Tthick', mods_list=mods_list)

        self.master.finished()

    ####################################################
    #   CURSOR WARP
    ####################################################

    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):
        '''Remove shader handle.'''

        if self.draw_handle:
            self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")


    def draw_shader(self, context):
        '''Draw shader handle.'''

        draw_modal_frame(context)


def bevels(obj, angle=False, weight=False, vertex_group=False, props={}):
    if not hasattr(obj, 'modifiers'):
        return []

    bevel_mods = [mod for mod in obj.modifiers if mod.type == 'BEVEL']

    if not angle and not weight and not vertex_group and not props:
        return bevel_mods

    modifiers = []
    limit_method_in = lambda method, mod: mod not in modifiers and mod.limit_method == method

    if angle:
        for mod in bevel_mods:
            if limit_method_in('ANGLE', mod):
                modifiers.append(mod)

    if weight:
        for mod in bevel_mods:
            if limit_method_in('WEIGHT', mod):
                #modifiers.append(mod)
                break

    if vertex_group:
        for mod in bevel_mods:
            if limit_method_in('VGROUP', mod):
                #modifiers.append(mod)
                break

    if props:

        for mod in bevel_mods:
            if mod in modifiers:
                continue

            for pointer in props:
                prop = hasattr(mod, pointer) and getattr(mod, pointer) == props[pointer]
                if not prop:
                    continue

                modifiers.append(mod)

    return sorted(modifiers, key=lambda mod: bevel_mods.index(mod))


def add_bevel_modifier(context, object, angle):
    bevel_modifier = object.modifiers.new(name="Bevel", type="BEVEL")
    bevels = [mod for mod in object.modifiers if mod.type == 'BEVEL']
    if addon.preference().ui.Hops_extra_info:
        if addon.preference().property.workflow_mode == 'ANGLE':
            bpy.ops.hops.display_notification(info=f'Bevel Added {"%.1f"%(degrees(angle))}° - Total : {len(bevels)}' )
        else:
            bpy.ops.hops.display_notification(info=f'Bevel Added - Total : {len(bevels)}' )
    bevel_modifier.limit_method = addon.preference().property.workflow_mode

    bevel_modifier.angle_limit = angle
    bevel_modifier.miter_outer = addon.preference().property.bevel_miter_outer
    bevel_modifier.miter_inner = addon.preference().property.bevel_miter_inner
    bevel_modifier.width = 0.01

    if bpy.app.version > (2, 89, 0):
        bevel_modifier.affect = 'EDGES'

    bevel_modifier.profile = addon.preference().property.bevel_profile
    bevel_modifier.loop_slide = addon.preference().property.bevel_loop_slide
    bevel_modifier.use_clamp_overlap = False
    bevel_modifier.segments = addon.preference().property.default_segments
    if object.dimensions[2] == 0 or object.dimensions[1] == 0 or object.dimensions[0] == 0:
        bevel_modifier.segments = 6
        if bpy.app.version < (2, 90, 0):
            bevel_modifier.use_only_vertices = True
        else:
            bevel_modifier.affect = 'VERTICES'
        bevel_modifier.use_clamp_overlap = True
        if addon.preference().ui.Hops_extra_info:
            bpy.ops.hops.display_notification(info=f'2d Bevel Added' )
    elif object.hops.status == "BOOLSHAPE":
        bevel_modifier.harden_normals = False
    # elif addon.preference().property.use_harden_normals:
    #     bevel_modifier.harden_normals = True
    object.show_all_edges = True

    if object.type == 'MESH':
        if not object.data.use_auto_smooth:
            object.data.auto_smooth_angle = radians(60)
        object.data.use_auto_smooth = True

        if context.mode == 'EDIT_MESH':
            vg = object.vertex_groups.new(name='HardOps')
            bpy.ops.object.vertex_group_assign()
            if addon.preference().property.adjustbevel_use_1_segment:
                if context.tool_settings.mesh_select_mode[2] and object.hops.status == "BOOLSHAPE":
                    bevel_modifier.segments = 1
                    bevel_modifier.harden_normals = False
                    bpy.ops.mesh.flip_normals()
            bevel_modifier.limit_method = 'VGROUP'
            bevel_modifier.vertex_group = vg.name
            bpy.ops.mesh.faces_shade_smooth()
            if addon.preference().ui.Hops_extra_info:
                bpy.ops.hops.display_notification(info=f'Vgroup Bevel Added' )
        else:
            ops.shade_smooth()

    # self.bevel_objects.setdefault(object.name, {})["modifier"] = bevel_modifier.name
    # self.bevel_objects[object.name]["added_modifier"] = True

    return (bevel_modifier.name, True)


def smooth_cutters_rec(objects):
    '''Recursively shade smooth cutters of objects and their cutters'''
    stack = list(objects)
    filter = set()

    while stack:
        obj = stack.pop()

        for mod in obj.modifiers:
            if mod.type == 'BOOLEAN': pass

            elif geometry_nodes.NodeBoolean.is_valid_modifier(mod):
                mod = geometry_nodes.NodeBoolean.new_unchecked(mod)
            else:
                continue

            if getattr(mod, 'operand_type', 'OBJECT') == 'OBJECT' and mod.object:
                if mod.object not in filter:
                    filter.add(mod.object)
                    stack.append(mod.object)

                    object.smooth_set(obj , True)

            elif mod.collection:
                meshes = [o for o in mod.collection.objects if o not in filter]
                filter.update(meshes)
                stack.extend(meshes)

                object.smooth_batch_set(meshes, True)