OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / booleans / bool_modal.py

438 lines · python ref:
import bpy
from . import operator
from . editmode_intersect import edit_bool_intersect
from . editmode_union import edit_bool_union
from . editmode_difference import edit_bool_difference
from . editmode_slash import edit_bool_slash
from . editmode_inset import edit_bool_inset
from . editmode_knife import edit_bool_knife
from ... utility import addon
from ... ui_framework.master import Master
from ... ui_framework.utils.mods_list import get_mods_list
from ... utility.base_modal_controls import Base_Modal_Controls
from ... utils.toggle_view3d_panels import collapse_3D_view_panels


OPERATIONS = ('INTERSECT', 'UNION', 'DIFFERENCE', 'SLASH', 'INSET', 'OUTSET', 'KNIFE')

DESC = """Bool Modal
LMB - Create a new boolean relationship
LMB + Ctrl - Ignore sort and keep bevel modifiers on inset objects if they don't use vertex groups or bevel weight"""

operation_items=[
    ('DIFFERENCE', "Difference", "Peform a difference operation"),
    ('INTERSECT', "Intersect", "Peform an intersect operation"),
    ('UNION', "Union", "Peform a union operation"),
    ('SLASH', "Slash", "Peform a slash operation"),
    ('INSET', "Inset", "Peform an inset operation"),
    ('OUTSET', "Outset", "Peform an outset operation"),
    ('KNIFE', "Knife Boolean", "Peform a knife boolean operation")
    #('KNIFE_PROJECT', "Knife Project", "Peform a knife project operation")],
]

operation_items_nodes=[
    ('DIFFERENCE', "Difference", "Peform a difference operation"),
    ('INTERSECT', "Intersect", "Peform an intersect operation"),
    ('UNION', "Union", "Peform a union operation"),
    ('SLASH', "Slash", "Peform a slash operation"),
    ('INSET', "Inset", "Peform an inset operation"),
    ('KNIFE', "Knife Boolean", "Peform a knife boolean operation"),
]

def operation_items_get(self, context):
    return operation_items_nodes if addon.preference().property.workflow_modifier == 'NODES' else operation_items

class HOPS_OT_BoolModal(bpy.types.Operator):
    bl_idname = "hops.bool_modal"
    bl_label = "Bool Modal"
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING', 'GRAB_CURSOR'}
    bl_description = DESC

    operation: bpy.props.EnumProperty(
        name="Operation",
        description="What kind of boolean operation to change to",
        items=operation_items_get)

    thickness: bpy.props.FloatProperty(
        name="Thickness",
        description="How deep the inset should cut",
        default=0.10,
        min=0.00,
        precision=3)

    thickness_nodes: bpy.props.FloatProperty(
        name="Thickness",
        description="How deep the inset should cut",
        default=0.10,
        precision=3)

    keep_bevels: bpy.props.BoolProperty(
        name="Keep Bevels",
        description="Keep Bevel modifiers on inset objects enabled if they don't use vertex groups or bevel weight",
        default=False)

    ignore_sort: bpy.props.BoolProperty(
        name="Ignore Sort",
        description="Ignore modifier sorting for this bool operation",
        default=False)

    inset_slice: bpy.props.BoolProperty(
        name="Inset Slice",
        description="Create Slice from inset volume",
        default=False)

    transfer_normals: bpy.props.BoolProperty(
        name="Transfer Normals",
        description="Write current normals of inputs as Custom Normals",
        default=False)

    weld: bpy.props.BoolProperty(
        name="Weld",
        description="Weld duplicate vertices after boolean",
        default=False)

    merge_threshold: bpy.props.FloatProperty(
        name="Merge Threshold",
        description="Distance below whcih vertices are merged",
        default=0.0001,
        min=0.00,
        precision=4)

    stitch_distance: bpy.props.FloatProperty(
        name="Stitch Distance",
        description="Distance below whcih vertices of new edges are stitched",
        default=0.0001,
        min=0.00,
        precision=4)

    non_manifold: bpy.props.BoolProperty(
        name="Non-manifold",
        description="Makes cutter double-sided enabling use of non-manifold geometry.",
        default=False)
    # Popover
    operator = None
    selected_operation = ""

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


    def draw(self, context):
        self.layout.prop(self, "operation")
        nodes = addon.preference().property.workflow_modifier == 'NODES'

        if self.operation in ('INSET', 'OUTSET'):
            self.layout.prop(self, "thickness" if not nodes else "thickness_nodes")

            if context.active_object.mode == 'OBJECT':
                row = self.layout.row()
                row.prop(self, "keep_bevels")
                row.prop(self, "ignore_sort")

                if self.operation == 'INSET':
                    row.prop(self, "inset_slice")

        elif context.active_object.mode == 'OBJECT':
            self.layout.prop(self, "ignore_sort")

        if nodes:
            if self.operation == 'KNIFE':
                row = self.layout.row()
                row.prop(self, "non_manifold")
                row.prop(self, "stitch_distance")
                self.layout.separator()

            row = self.layout.row()
            row.prop(self, "transfer_normals")
            row = self.layout.row()
            row.prop(self, "weld")
            row = row.split()
            row.enabled = self.weld
            row.prop(self, "merge_threshold")


    def invoke(self, context, event):
        # Unlike bool_shift, it is not necessary to push an undo step here
        try:
            kc = context.window_manager.keyconfigs
            km = kc.user.keymaps["3D View"]
            kmi = km.keymap_items["hops.bev_multi"]
            hotkey = kmi.type

        except:
            hotkey = None

        if event.type != hotkey:
            self.keep_bevels = event.ctrl
            self.ignore_sort = event.ctrl

            if event.shift:
                if self.operation == 'INSET':
                    self.operation = 'OUTSET'

                if self.operation == 'KNIFE':
                    self.operation = 'KNIFE_PROJECT'

        self.inset_slice = False
        self.report_info(self.operation)
        self.get_overlays(context)
        self.execute(context)
        self.thick_adjust = False

        global OPERATIONS
        OPERATIONS = [itm[0] for itm in operation_items_get(None, None)]

        #UI System
        self.master = Master(context=context)
        self.master.only_use_fast_ui = True
        self.timer = context.window_manager.event_timer_add(0.025, window=context.window)
        self.base_controls = Base_Modal_Controls(context, event, popover_keys=['TAB', 'SPACE'])
        self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
        context.window_manager.modal_handler_add(self)
        self.solidfy_list = []
        self.nodes_list = []

        # Popover
        self.__class__.operator = self
        self.__class__.selected_operation = ""

        return {"RUNNING_MODAL"}


    def modal(self, context, event):

        # UI
        self.master.receive_event(event)
        self.base_controls.update(context, event)

        # Popover
        self.popover(context)

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

        elif self.base_controls.confirm:
            self.report_info('FINISHED')
            self.common_exit(context)
            return {'FINISHED'}

        elif self.base_controls.cancel:
            self.report_info('CANCELLED')
            self.cancel(context)
            self.common_exit(context)
            return {'CANCELLED'}

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

        elif event.type == 'T' and event.value == 'PRESS' :
            self.thick_adjust = not self.thick_adjust

        elif self.base_controls.mouse and self.thick_adjust and self.operation in {'INSET', 'OUTSET'}:
            self.thickness +=self.base_controls.mouse
            self.thickness_nodes +=self.base_controls.mouse

            for mod in self.solidfy_list:
                mod.thickness = self.thickness

            for mod in self.nodes_list:
                mod.inset_thickness = self.thickness_nodes

        elif self.base_controls.scroll or (event.type in {'X', 'Z'} and event.value == 'PRESS'):
            if event.type == 'X':
                self.scroll(1)
            elif event.type == 'Z':
                self.scroll(-1)
            else:
                self.scroll(self.base_controls.scroll)

            self.report_info(self.operation)
            self.cancel(context)
            self.execute(context)

        self.draw_modal(context)
        context.area.tag_redraw()
        return {"RUNNING_MODAL"}


    def execute(self, context):
        if addon.preference().property.workflow_modifier == 'NODES':

            if self.operation == 'KNIFE_PROJECT':
                operator.knife(context, True, material_cut=True)
                self.set_overlays(context)
            else:
                # this returns node boolean wrapper
                self.nodes_list = operator.add(context, operation=self.operation, sort=not self.ignore_sort, thickness=self.thickness_nodes, keep_bevels=self.keep_bevels, inset_slice=self.inset_slice, transfer_normals=self.transfer_normals, weld=self.weld, merge_threshold=self.merge_threshold, stitch_distance=self.stitch_distance)
                self.set_overlays(context)

            return {'FINISHED'}


        if context.active_object.mode == 'OBJECT':

            if self.operation in {'INTERSECT', 'UNION', 'DIFFERENCE', 'SLASH', 'INSET'}:
                self.solidfy_list = operator.add(context, self.operation, sort=not self.ignore_sort, outset=False, thickness=self.thickness, inset_slice=self.inset_slice)
                self.reset_overlays(context)

            elif self.operation == 'OUTSET':
                self.solidfy_list = operator.add(context, 'INSET', sort=not self.ignore_sort, outset=True, thickness=self.thickness)
                self.reset_overlays(context)

            elif self.operation == 'KNIFE':
                operator.knife(context, False)
                self.set_overlays(context)

            elif self.operation == 'KNIFE_PROJECT':
                operator.knife(context, True, material_cut=True)
                self.set_overlays(context)

        elif context.active_object.mode == 'EDIT':
            if self.operation == 'INTERSECT':
                edit_bool_intersect(context, False, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'UNION':
                edit_bool_union(context, False, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'DIFFERENCE':
                edit_bool_difference(context, False, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'SLASH':
                edit_bool_slash(context, False, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'INSET':
                edit_bool_inset(context, False, False, self.thickness, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'OUTSET':
                edit_bool_inset(context, False, True, self.thickness, False, False, 0.0, addon.preference().property.boolean_solver)
                self.reset_overlays(context)

            elif self.operation == 'KNIFE':
                edit_bool_knife(context, False, False)
                self.reset_overlays(context)

            elif self.operation == 'KNIFE_PROJECT':
                edit_bool_knife(context, False, True)
                self.reset_overlays(context)

        return {'FINISHED'}


    def cancel(self, context):
        bpy.ops.ed.undo_push()
        bpy.ops.ed.undo()


    def report_info(self, info):
        words = [w.capitalize() for w in str(info).split("_")]
        self.report({'INFO'}, " ".join(words))


    def get_overlays(self, context):
        self.overlays = context.space_data.overlay.show_overlays
        self.wireframes = context.space_data.overlay.show_wireframes


    def set_overlays(self, context):
        context.space_data.overlay.show_overlays = True
        context.space_data.overlay.show_wireframes = True


    def reset_overlays(self, context):
        context.space_data.overlay.show_overlays = self.overlays
        context.space_data.overlay.show_wireframes = self.wireframes


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


    def draw_modal(self, context):

        # Make the operation text look nice
        operation = " ".join([w.capitalize() for w in str(self.operation).split("_")])

        self.master.setup()
        if not self.master.should_build_fast_ui(): return

        # Main
        win_list = []
        if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1:
            win_list.append(operation)
            if self.operation in ('INSET', 'OUTSET'):
                win_list.append(f"{self.thickness:.3f}")
        else:
            win_list.append("Interactive Boolean")
            win_list.append(f"Mode : {operation}")
            if self.operation in ('INSET', 'OUTSET'):
                win_list.append(f"Thickness : {self.thickness:.3f}")
                if context.active_object.mode == 'OBJECT':
                    win_list.append(f"Keep Bevels : {self.keep_bevels}")
                    win_list.append(f"Ignore Sort : {self.ignore_sort}")
            elif context.active_object.mode == 'OBJECT':
                win_list.append(f"Ignore Sort : {self.ignore_sort}")

        # 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"] = [
            ("T",  "Thickness Adjust"),
            ("Wheel",  "Switch Operation"),
            ("X / Z",  "Switch Operation")
        ]

        self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="InteractiveBoolean")
        self.master.finished()


    def common_exit(self, context):
        self.reset_overlays(context)
        collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
        self.master.run_fade()
        self.__class__.operator = self
        self.__class__.selected_operation = ""


    def popover(self, context):
        # Popup captured new mod
        if self.__class__.selected_operation != "":
            if self.__class__.selected_operation in OPERATIONS:
                self.operation = self.__class__.selected_operation
                self.report_info(self.operation)
                self.cancel(context)
                self.execute(context)
                bpy.ops.hops.display_notification(info=f'Operation : {self.__class__.selected_operation}')
            self.__class__.selected_operation = ""

        # Spawns
        if self.base_controls.popover:
            context.window_manager.popover(popup_draw)

# --- POPOVER --- #

def popup_draw(self, context):
    layout = self.layout
    layout.label(text='Selector')
    broadcaster = "hops.popover_data"
    for operation in OPERATIONS:
        row = layout.row()
        row.scale_y = 2
        props = row.operator(broadcaster, text=operation.title())
        props.calling_ops = 'BOOL_MODAL'
        props.str_1 = operation