OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / src / operator / shape / sharpen.py

590 lines · python ref:
import bpy
from math import radians, degrees
from bpy.props import EnumProperty, IntProperty, FloatProperty, BoolProperty
from .... operators.utils import update_Weight_modifier_if_necessary, set_smoothing, mark_ssharps_bmesh, update_bevel_modifier_if_necessary
from .... utility import addon, modifier, ct_modifier
from ....ui_framework.operator_ui import Master
from .... operators.misc.late_parent import late_parent
from .... operators.meshtools.applymod import apply_mod as smart_apply

def iterate_titled_as_string(iter, separator=','):
    applied_types = ''
    separator = f'{separator} '
    for i in iter:
        applied_types += i.title() + separator
    return applied_types[:-len(separator)]

class HOPS_OT_Sharpen(bpy.types.Operator):
    bl_idname = "hops.sharpen"
    bl_label = "Hops sharpen"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'WINDOW'
    bl_options = {"REGISTER", "UNDO"}
    bl_description = """ Sharpen - Shading Assistant

LMB            -  Sharpen (Ssharp)
Ctrl             -  Sharpen / Apply Mods (CSharp)
Shift            -  Autosmooth (Interactive)
Alt              -  Weighted Normal
Ctrl+Shift     -  Recalculate (Resharp)

"""
#Sharpen refers to marking specific angles ex: 30º

    items: [(x.identifier, x.name, x.description, x.icon)
            for x in bpy.types.Modifier.bl_rna.properties['type'].enum_items]

    behavior: EnumProperty(
        name="Operator Modes",
        default='SSHARP',
        items=(("SSHARP", "Ssharp", ""),
               ("CSHARP", "Csharp", ""),
               ("RESHARP", "Resharp", ""),
               ("CSHARPBEVEL", "CsharpBevel", ""),
               ("SSHARPWN", "SsharpWN", ""),
               ("AUTOSMOOVE", "Autosmooth", ""),
               ("CLEANSHARP", "Cleansharp", ""),
               ("SMART_APPLY", "Smart Apply", "")))

    mode: EnumProperty(
        name="Operator Modes",
        default='SSHARP',
        items=(("SSHARP", "Ssharp", ""),
               ("CSHARP", "Csharp", ""),
               ("CLEANSHARP", "Cleansharp", "")))

    boolean: BoolProperty(name="boolean", default=True)

    mirror: BoolProperty(name="mirror", default=False)

    bevel: BoolProperty(name="bevel", default=False)

    solidify: BoolProperty(name="solidify", default=True)

    array: BoolProperty(name="array", default=False)

    others: BoolProperty(name="others", default=False)

    bevel_vg: BoolProperty(name="only bevel with vertex group", default=True)

    #segment_amount: IntProperty(name="Segments", description="Segments For Bevel", default=3, min=1, max=12)

    bevelwidth: FloatProperty(name="Bevel Width Amount",
                              description="Set Bevel Width",
                              default=0.0200,
                              precision=3,
                              min=0.000,
                              max=100)

    #segment_modal: IntProperty(name="Segments", description="Segments For Bevel", default=3, min=1, max=12)

    bevelwidth_modal: FloatProperty(name="Bevel Width Global",
                                    description="Set Global Bevel Width",
                                    default=0.0200,
                                    precision=3,
                                    min=0.000,
                                    max=100)

    add_bevel: BoolProperty(name="Add Bevel",
                            description="Add Bevel Modifier if not there",
                            default=False)

    additive_mode: BoolProperty(name="Additive Mode",
                                description="Don't clear existing edge properties",
                                default=True)

    auto_smooth_angle: FloatProperty(name="angle edge marks are applied to",
                                     default=radians(60),
                                     min=radians(1),
                                     max=radians(180),
                                     precision=3,
                                     unit='ROTATION')

    removeMods: BoolProperty(default=False)

    clearsharps: BoolProperty(default=True)

    clearbevel: BoolProperty(default=True)

    clearcrease: BoolProperty(default=True)

    clearseam: BoolProperty(default=True)

    clearcustomdata: BoolProperty(default=False)

    use_resharp: BoolProperty(name="triggers resharp notification", default=False)

    text = "Sharps Removed"

    reveal_mesh = True

    called_ui = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        HOPS_OT_Sharpen.called_ui = False

    @classmethod
    def poll(cls, context):

        selected = context.selected_objects
        object = context.active_object
        if object is None: return False
        if object.mode == "OBJECT" and all(obj.type == "MESH" for obj in selected):
            return True


    def draw(self, context):

        layout = self.layout

        col = layout.column()

        colrow = col.row(align=True)
        if self.behavior != 'SMART_APPLY':
            colrow.prop(self, "mode", expand=True)
        else:
            col.label(text=self.behavior)

        col.separator()

        if self.mode == 'CSHARP':
            col.prop(self, "add_bevel", text='Add bevel modifier', toggle=True, expand=True)

            col.label(text="Modifiers Applied By Csharp")

            colrow = col.row(align=True)
            colrow.prop(self, "boolean", text='Bool', toggle=True)
            colrow.prop(self, "solidify", text='Sol', toggle=True)
            colrow.prop(self, "bevel", text='Bev', toggle=True)
            colrow.prop(self, "mirror", text='Mir', toggle=True)
            colrow.prop(self, "array", text='Arr', toggle=True)
            colrow.prop(self, "others", text='Others', toggle=True)
            col.separator()

            colrow = col.row(align=True)
            colrow.prop(addon.preference().property, "Hops_sharp_remove_cutters", text='Remove Cutters', toggle=True)

            col.separator()
            if self.bevel:
                colrow = col.row(align=True)
                colrow.prop(self, "bevel_vg", text='Vertex Group bevels applied only')

        if self.mode in{'SSHARP'}:
            col.separator()
            col.separator()
            col.separator()
            col.separator()
            col.separator()
            col.separator()
            col.separator()

        if self.mode in {'SSHARP', 'CSHARP'} and self.behavior != 'SMART_APPLY':
            col.label(text="Sharpening Parameters")
            col = layout.column(align=True)
            col.use_property_split = True
            col.prop(self, "additive_mode")
            col.prop(addon.preference().property, "sharpness", text="Sharpness")
            col.prop(addon.preference().property, "auto_smooth_angle", text="Smooth Angle")


        if self.mode == 'CLEANSHARP':
            col.separator()
            col.prop(self, 'removeMods', text="RemoveModifiers?")
            col.prop(self, 'clearsharps', text="Clear Sharps")
            col.prop(self, 'clearbevel', text="Clear Bevels")
            col.prop(self, 'clearcrease', text="Clear Crease")
            col.prop(self, 'clearseam', text="Clear Seam")
            col.prop(self, 'clearcustomdata', text="Clear Custom Data")
            col.separator()
            col.separator()


    def invoke(self, context, event):

        self.auto_smooth_angle = context.active_object.data.auto_smooth_angle

        self.apply_mods = True

        if event.shift and not event.ctrl and not event.alt:
            self.behavior = addon.preference().keymap.sharp_shift
        elif event.ctrl and not event.shift and not event.alt:
            self.behavior = addon.preference().keymap.sharp_ctrl
        elif event.alt and not event.shift and not event.ctrl:
            self.behavior = addon.preference().keymap.sharp_alt
        elif event.ctrl and event.shift and not event.alt:
            self.behavior = addon.preference().keymap.sharp_shift_ctrl
        elif event.ctrl and event.alt and not event.shift:
            self.behavior = addon.preference().keymap.sharp_alt_ctrl
        elif event.shift and event.alt and not event.ctrl:
            self.behavior = addon.preference().keymap.sharp_alt_shift
        else:
            self.behavior = addon.preference().keymap.sharp

        if self.behavior in {'SSHARP', 'SSHARPWN', 'AUTOSMOOVE', 'SMART_APPLY'}:
            self.mode = 'SSHARP'
            self.additive_mode = True
        elif self.behavior == 'RESHARP':
            self.mode = 'SSHARP'
            self.additive_mode = False
        elif self.behavior == 'CSHARP':
            self.mode = 'CSHARP'
        elif self.behavior == 'CLEANSHARP':
            self.mode = 'CLEANSHARP'
        elif self.behavior == 'CSHARPBEVEL':
            self.mode = 'CSHARP'

            #return {"FINISHED"}

        self.execute(context)

        if self.behavior == 'CSHARPBEVEL':
            bpy.ops.hops.adjust_bevel("INVOKE_DEFAULT", ignore_ctrl=True)

        if self.mode == 'CSHARP':
            if addon.preference().property.auto_bweight:
                bpy.ops.hops.adjust_bevel("INVOKE_DEFAULT", ignore_ctrl=True)

        return {"FINISHED"}


    def execute(self, context):
        applied = []
        selected = context.selected_objects
        # object = context.active_object

        for obj in selected:

            if self.mode == 'SSHARP':

                if self.behavior == 'SSHARPWN':
                    for mod in obj.modifiers:
                        if mod.type == 'WEIGHTED_NORMAL':
                            obj.modifiers.remove(mod)


                    prefix = addon.preference().property.mod_prefix_WN
                    mod = obj.modifiers.new(name=f"{prefix}Weighted Normal", type='WEIGHTED_NORMAL')
                    if mod and hasattr(mod, "use_pin_to_last"):
                        mod.use_pin_to_last = True
                        ct_modifier.mod_move_last(mod, force=True)
                        ct_modifier.update_object(obj, context)
                    mod.keep_sharp = True
                    self.report({'INFO'}, F'Weighted Normal')

                elif self.behavior == 'AUTOSMOOVE':
                    bpy.ops.hops.adjust_auto_smooth("INVOKE_DEFAULT")

                elif self.behavior == 'SMART_APPLY':
                    for self.obj in bpy.context.selected_objects:
                        smart_apply(self, self.obj, clear_last=False)
                        bpy.ops.hops.draw_wire_mesh_launcher('INVOKE_DEFAULT', target='SELECTED')

                else:
                    soft_sharpen_object(
                        self,
                        obj,
                        addon.preference().property.sharpness,
                        addon.preference().property.auto_smooth_angle,
                        self.additive_mode,
                        self.reveal_mesh,)
                    if self.additive_mode is False:
                        self.report({'INFO'}, F'Resharpened')
                    else:
                        if self.behavior == 'SSHARP':
                            self.report({'INFO'}, F'SSharpened')

                    # self.report({'INFO'}, F'Weighted Normal')

            elif self.mode == 'CSHARP':
                applied = complex_sharpen_active_object(
                    self,
                    obj,
                    addon.preference().property.sharpness,
                    addon.preference().property.auto_smooth_angle,
                    self.additive_mode,
                    self.reveal_mesh,
                    self.apply_mods,
                    self.add_bevel)
                self.report({'INFO'}, F'CSharpened ')

            elif self.mode == 'CLEANSHARP':
                clear_ssharps_active_object(
                    context,
                    obj,
                    self.removeMods,
                    self.clearsharps,
                    self.clearbevel,
                    self.clearcrease,
                    self.clearseam,
                    self.clearcustomdata,
                    self.text)
                self.report({'INFO'}, F'Sharps Unmarked')

        # Operator UI
        if not HOPS_OT_Sharpen.called_ui:
            HOPS_OT_Sharpen.called_ui = True

            ui = Master()
            #not_applied = [mod for mod in context.active_object.modifiers]
            if not context.active_object:
                applied = '0'

            if self.behavior == "CSHARP":
                draw_data = [
                    ["Csharp (apply)"],
                    #["Additive Mode", self.additive_mode],
                    ["Bevel Added ",  (self.add_bevel)],
                    ["Cutters Removed ", (addon.preference().property.Hops_sharp_remove_cutters)],
                    ["Auto Smooth ", "%.0f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                    ["Sharpness   ", "%.0f" % degrees(addon.preference().property.sharpness) + "°"],
                    #["Modifiers Applied : ", iterate_titled_as_string([mod.name for mod in applied]) if applied else ''],
                    #["Modifiers Remaining", len(context.active_object.modifiers[:])],
                    [f"Modifiers Applied / Remain", f"{len(applied)} / {len(context.active_object.modifiers[:])}"],
                    ]

                if len(context.active_object.modifiers[:]) >= 1:
                    draw_data.insert(-5, ["Remaining Modifiers ", len(context.active_object.modifiers[:])]),
                    for mod in reversed(context.active_object.modifiers):
                        draw_data.insert(-6, [" ", mod.name]),

                # for i, mod in enumerate(reversed(applied)):
                #     if i < 3:
                #         try: draw_data.insert(-1, [" ", mod.name])
                #         except: pass
                #     else:
                #         #draw_data.insert(-1, [" ", '...'])
                #         break
            elif self.behavior == "RESHARP":
                draw_data = [
                    ["Resharp (shading)"],
                    ["Auto Smooth", "%.1f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                    ["Sharpness", "%.1f" % degrees(addon.preference().property.sharpness) + "°"],
                    ["Additive Mode", self.additive_mode],
                    ["Sharpening Recalculated"]]
            elif self.behavior == "CSHARPBEVEL":
                draw_data = [
                    ["Csharp to Bevel"],
                    ["Cutters Removed", (addon.preference().property.Hops_sharp_remove_cutters)],
                    ["Modifiers Applied", len(applied)],
                    ["Bevel Initialzied"]]
            elif self.behavior == "SSHARPWN":
                draw_data = [
                    ["Weighted Normal"],
                    ["Weighted Normal Mod w/ Keep Sharp Added"]]
            elif self.behavior == "CLEANSHARP":
                draw_data = [
                    ["Sharps Cleared"],
                    ["Clear Custom Normal Data", self.clearcustomdata],
                    ["Clear Seam ", self.clearseam],
                    ["Clear Crease ", self.clearcrease],
                    ["Clear Bevel ", self.clearbevel],
                    ["Clear Sharps ", self.clearsharps]]
            elif self.behavior == "SSHARP":
                if addon.preference().property.parent_boolshapes and (self.parent_details[1] * self.parent_details[2]) != 0:
                    draw_data = [
                        ["Sharpen + Parent"],
                        ["Modifiers Applied", "0"],
                        ["Parent Shapes  ",  (addon.preference().property.parent_boolshapes)],
                        ["Bools Total / Parented", F"{self.parent_details[2]} / {self.parent_details[1]}"],
                        ["Additive Mode", self.additive_mode],
                        ["Auto Smooth", "%.1f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                        ["Sharpness", "%.1f" % degrees(addon.preference().property.sharpness) + "°"]]
                else:
                    draw_data = [
                        ["Sharpen"],
                        ["Modifiers Applied", "0"],
                        ["Additive Mode", self.additive_mode],
                        ["Auto Smooth", "%.1f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                        ["Sharpness", "%.1f" % degrees(addon.preference().property.sharpness) + "°"]]

                    # if addon.preference().property.sharp_use_crease:
                    #     draw_data.insert(1, ["Mark Crease", addon.preference().property.sharp_use_crease])
                    # if addon.preference().property.sharp_use_sharp:
                    #     draw_data.insert(1, ["Mark Sharp", addon.preference().property.sharp_use_sharp])
                    # if addon.preference().property.sharp_use_bweight:
                    #     draw_data.insert(1, ["Mark Bevel Weight", addon.preference().property.sharp_use_bweight])
                    # if addon.preference().property.sharp_use_seam:
                    #     draw_data.insert(1, ["Mark UV Seam", addon.preference().property.sharp_use_seam])
            elif self.behavior == "AUTOSMOOVE":
                draw_data = [
                ['Autosmooth '],
                ["Additive Mode", self.additive_mode],
                ["Auto Smooth", "%.1f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                ["Sharpness", "%.1f" % degrees(addon.preference().property.sharpness) + "°"]
                ]
            elif self.behavior == "SMART_APPLY":
                draw_data = [
                ['Smart Apply '],
                ["Remaining Mods", len(context.active_object.modifiers[:])]
                ]
            else:
                draw_data = [
                    [self.behavior],
                    ["Additive Mode", self.additive_mode],
                    ["Auto Smooth", "%.1f" % degrees(context.active_object.data.auto_smooth_angle) + "°"],
                    ["Sharpness", "%.1f" % degrees(addon.preference().property.sharpness) + "°"]
                    ]

            active_object = context.active_object
            if active_object.scale[2] != 1 or active_object.scale[1] != 1 or active_object.scale[0] != 1:
                draw_data.insert(1, ["WARNING", "Scale unapplied"])

            ui.receive_draw_data(draw_data=draw_data)
            ui.draw(draw_bg=addon.preference().ui.Hops_operator_draw_bg, draw_border=addon.preference().ui.Hops_operator_draw_border)

        del applied
        return {"FINISHED"}


def soft_sharpen_object(self, object, sharpness, auto_smooth_angle, additive_mode, reveal_mesh):

    mark_ssharps_bmesh(object, sharpness, reveal_mesh, additive_mode)
    set_smoothing(object, auto_smooth_angle)


    if self.behavior == "SSHARP":
        if addon.preference().property.parent_boolshapes:
            self.parent_details = late_parent(bpy.context)


def complex_sharpen_active_object(self, obj, sharpness, auto_smooth_angle, additive_mode, reveal_mesh, apply_mods, add_bevel):

    apply = None
    if apply_mods:
        apply = apply_mod(self, obj)
        #bpy.ops.mesh.customdata_custom_splitnormals_clear()

    soft_sharpen_object(self, obj, sharpness, auto_smooth_angle, additive_mode, reveal_mesh)

    if add_bevel:
        update_bevel_modifier_if_necessary(
            obj,
            addon.preference().property.default_segments,
            self.bevelwidth,
            addon.preference().property.bevel_profile)

    #if addon.preference().property.add_weighten_normals_mod:
    #    update_Weight_modifier_if_necessary(obj)

    if len(bpy.context.selected_objects) == 1:
        bpy.ops.hops.draw_wire_mesh_launcher('INVOKE_DEFAULT', target='SELECTED')

    return apply


def apply_mod(self, object):

    mod_types = []
    others = []
    bevel = []
    last_bevel = None
    if self.boolean:
        mod_types.append('BOOLEAN')
    if self.mirror:
        mod_types.append('MIRROR')
    if self.bevel:
        if self.bevel_vg:
            bevel = [mod for mod in object.modifiers if (mod.type in "BEVEL") and mod.vertex_group]
        else:
            mod_types.append('BEVEL')
            bevels = [mod for mod in object.modifiers if (mod.type in "BEVEL")]
            if bevels:
                last_bevel = bevels[-1]
    if self.solidify:
        mod_types.append('SOLIDIFY')
    if self.array:
        mod_types.append('ARRAY')

    mods = [mod for mod in object.modifiers if mod.type in mod_types]

    if addon.preference().property.Hops_sharp_remove_cutters:
        cutters = [modifier.bool_object_get(mod) for mod in object.modifiers if modifier.mod_type(mod) == 'BOOLEAN']
        cutters = {o for o in cutters if o}

    if self.others:
        others = [mod for mod in object.modifiers if modifier.mod_type(mod) not in {'BOOLEAN', 'MIRROR', 'BEVEL', 'SOLIDIFY', 'ARRAY'}]
    mod = mods + others + bevel
    if last_bevel:
        mod.remove(last_bevel)
    if mod:
        modifier.apply(object, modifiers=mod)

    if addon.preference().property.Hops_sharp_remove_cutters:
        for cutter in cutters:
            try:
                bpy.data.objects.remove(cutter)
            except:
                addon.preference().property.Hops_sharp_remove_cutters = False
                self.report({'ERROR_INVALID_INPUT'}, F'Cannot remove same Cutter from multiple objects')

    return mod
    #print(len(mod))
    #print(mod)
            # del cutter


def clear_ssharps_active_object(context, obj, removeMods, clearsharps, clearbevel, clearcrease, clearseam, clearcustomdata, text):

    remove_mods_shadeflat(removeMods, obj)
    clear_sharps(
        context,
        clearsharps,
        clearbevel,
        clearcrease,
        clearseam,
        clearcustomdata)
    object = bpy.context.active_object
    object.hops.status = "UNDEFINED"
    try:
        bpy.data.collections['Hardops'].objects.unlink(object)
    except:
        pass
    bpy.ops.object.shade_flat()
    bpy.context.object.data.use_auto_smooth = False


def clear_sharps(context, clearsharps, clearbevel, clearcrease, clearseam, clear_custom_data):

    active = bpy.context.active_object
    for obj in context.selected_objects:
        if obj.type == 'MESH':

            bpy.context.view_layer.objects.active = obj

            bpy.ops.object.mode_set(mode='EDIT')
            bpy.ops.mesh.reveal()
            bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE')
            bpy.ops.mesh.select_all(action='DESELECT')
            bpy.ops.mesh.select_all(action='TOGGLE')

            if clearsharps:
                bpy.ops.mesh.mark_sharp(clear=True)
            if clearbevel:
                bpy.ops.transform.edge_bevelweight(value=-1)
            if clearcrease:
                bpy.ops.transform.edge_crease(value=-1)
            if clearseam:
                bpy.ops.mesh.mark_seam(clear=True)
            if clear_custom_data:
                bpy.ops.mesh.customdata_custom_splitnormals_clear()
            bpy.ops.object.editmode_toggle()
    bpy.context.view_layer.objects.active = active


def remove_mods_shadeflat(removeMods, obj):

    if removeMods:
        # bevel = obj.modifiers.get("Bevel")
        # solidify = obj.modifiers.get("Solidify")
        # if bevel:
        #     obj.modifiers.remove(bevel)
        # if solidify:
        #     obj.modifiers.remove(obj.modifiers.get(solidify))
        for mod in obj.modifiers:
            if mod.type == 'WEIGHTED_NORMAL' or 'BEVEL' or 'SOLIDIFY':
                obj.modifiers.remove(mod)
    else:
        return {"FINISHED"}