Skip to content
Snippets Groups Projects
object_boolean_tools.py 40.11 KiB
# SPDX-License-Identifier: GPL-2.0-or-later

bl_info = {
    "name": "Bool Tool",
    "author": "Vitor Balbio, Mikhail Rachinskiy, TynkaTopi, Meta-Androcto, Simon Appelt",
    "version": (0, 4, 1),
    "blender": (2, 80, 0),
    "location": "View3D > Sidebar > Edit Tab",
    "description": "Bool Tool Hotkey: Ctrl Shift B",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/object/bool_tools.html",
    "category": "Object",
}

import bpy
from bpy.types import (
    AddonPreferences,
    Operator,
    Panel,
    Menu,
)
from bpy.props import (
    BoolProperty,
    StringProperty,
)


# -------------------  Bool Tool FUNCTIONS -------------------------
# Utils:

# Hide boolean objects
def update_BoolHide(self, context):
    ao = context.view_layer.objects.active
    objs = [i.object for i in ao.modifiers if i.type == "BOOLEAN"]
    hide_state = context.scene.BoolHide

    for o in objs:
        o.hide_viewport = hide_state


def isCanvas(_obj):
    try:
        if _obj["BoolToolRoot"]:
            return True
    except:
        return False


def isBrush(_obj):
    try:
        if _obj["BoolToolBrush"]:
            return True
    except:
        return False


# TODO
# def isPolyBrush(_obj):
#     try:
#         if _obj["BoolToolPolyBrush"]:
#             return True
#     except:
#         return False


def object_visibility_set(ob, value=False):
    ob.visible_camera = value
    ob.visible_diffuse = value
    ob.visible_glossy = value
    ob.visible_shadow = value
    ob.visible_transmission = value
    ob.visible_volume_scatter = value


def BT_ObjectByName(obj):
    for ob in bpy.context.view_layer.objects:
        if isCanvas(ob) or isBrush(ob):
            if ob.name == obj:
                return ob


def FindCanvas(obj):
    for ob in bpy.context.view_layer.objects:
        if isCanvas(ob):
            for mod in ob.modifiers:
                if "BTool_" in mod.name:
                    if obj.name in mod.name:
                        return ob


def isFTransf():
    preferences = bpy.context.preferences
    addons = preferences.addons
    addon_prefs = addons[__name__].preferences
    if addon_prefs.fast_transform:
        return True
    else:
        return False


def ConvertToMesh(obj):
    act = bpy.context.view_layer.objects.active
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.convert(target="MESH")
    bpy.context.view_layer.objects.active = act


# Do the Union, Difference and Intersection Operations with a Brush
def Operation(context, _operation):
    prefs = context.preferences.addons[__name__].preferences
    useWire = prefs.use_wire

    for selObj in context.selected_objects:
        if (
            selObj != context.active_object and
            (selObj.type == "MESH" or selObj.type == "CURVE")
        ):
            if selObj.type == "CURVE":
                ConvertToMesh(selObj)
            actObj = context.active_object
            selObj.hide_render = True

            if useWire:
                selObj.display_type = "WIRE"
            else:
                selObj.display_type = "BOUNDS"

            object_visibility_set(selObj, value=False)

            if _operation == "SLICE":
                # copies instance_collection property(empty), but group property is empty (users_group = None)
                clone = actObj.copy()
                context.collection.objects.link(clone)

                space_data = context.space_data
                is_local_view = bool(space_data.local_view)

                if is_local_view:
                    clone.local_view_set(space_data, True)

                sliceMod = clone.modifiers.new("BTool_" + selObj.name, "BOOLEAN")  # add mod to clone obj
                sliceMod.object = selObj
                sliceMod.operation = "DIFFERENCE"
                clone["BoolToolRoot"] = True

            newMod = actObj.modifiers.new("BTool_" + selObj.name, "BOOLEAN")
            newMod.object = selObj

            if _operation == "SLICE":
                newMod.operation = "INTERSECT"
            else:
                newMod.operation = _operation

            actObj["BoolToolRoot"] = True
            selObj["BoolToolBrush"] = _operation
            selObj["BoolTool_FTransform"] = "False"


# Remove Objects form the BoolTool System
def Remove(context, thisObj_name, Prop):
    # Find the Brush pointed in the Tree View and Restore it, active is the Canvas
    actObj = context.active_object

    # Restore the Brush
    def RemoveThis(_thisObj_name):
        for obj in bpy.context.view_layer.objects:
            # if it's the brush object
            if obj.name == _thisObj_name:
                obj.display_type = "TEXTURED"
                del obj["BoolToolBrush"]
                del obj["BoolTool_FTransform"]
                object_visibility_set(obj, value=True)

                # Remove it from the Canvas
                for mod in actObj.modifiers:
                    if "BTool_" in mod.name:
                        if _thisObj_name in mod.name:
                            actObj.modifiers.remove(mod)

    if Prop == "THIS":
        RemoveThis(thisObj_name)

    # If the remove was called from the Properties:
    else:
        # Remove the Brush Property
        if Prop == "BRUSH":
            Canvas = FindCanvas(actObj)

            if Canvas:
                for mod in Canvas.modifiers:
                    if "BTool_" in mod.name and actObj.name in mod.name:
                        Canvas.modifiers.remove(mod)

            actObj.display_type = "TEXTURED"
            del actObj["BoolToolBrush"]
            del actObj["BoolTool_FTransform"]
            object_visibility_set(actObj, value=True)

        if Prop == "CANVAS":
            for mod in actObj.modifiers:
                if "BTool_" in mod.name:
                    RemoveThis(mod.object.name)


# Toggle the Enable the Brush Object Property
def EnableBrush(context, objList, canvas):
    for obj in objList:
        for mod in canvas.modifiers:
            if "BTool_" in mod.name and mod.object.name == obj:

                if mod.show_viewport:
                    mod.show_viewport = False
                    mod.show_render = False
                else:
                    mod.show_viewport = True
                    mod.show_render = True


# Find the Canvas and Enable this Brush
def EnableThisBrush(context, set):
    canvas = None
    for obj in bpy.context.view_layer.objects:
        if obj != bpy.context.active_object:
            if isCanvas(obj):
                for mod in obj.modifiers:
                    if "BTool_" in mod.name:
                        if mod.object == bpy.context.active_object:
                            canvas = obj

    for mod in canvas.modifiers:
        if "BTool_" in mod.name:
            if mod.object == bpy.context.active_object:
                if set == "None":
                    if mod.show_viewport:
                        mod.show_viewport = False
                        mod.show_render = False
                    else:
                        mod.show_viewport = True
                        mod.show_render = True
                else:
                    if set == "True":
                        mod.show_viewport = True
                    else:
                        mod.show_viewport = False
                return


# Toggle the Fast Transform Property of the Active Brush
def EnableFTransf(context):
    actObj = bpy.context.active_object

    if actObj["BoolTool_FTransform"] == "True":
        actObj["BoolTool_FTransform"] = "False"
    else:
        actObj["BoolTool_FTransform"] = "True"
    return


# Apply All Brushes to the Canvas
def ApplyAll(context, list):
    objDeleteList = []
    for selObj in list:
        if isCanvas(selObj) and selObj == context.active_object:
            for mod in selObj.modifiers:
                if "BTool_" in mod.name:
                    objDeleteList.append(mod.object)
                try:
                    bpy.ops.object.modifier_apply(modifier=mod.name)
                except:  # if fails the means it is multiuser data
                    context.active_object.data = context.active_object.data.copy()  # so just make data unique
                    bpy.ops.object.modifier_apply(modifier=mod.name)
            del selObj["BoolToolRoot"]

    for obj in context.scene.objects:
        if isCanvas(obj):
            for mod in obj.modifiers:
                # do not delete brush that is used by another canvas
                if mod.type == "BOOLEAN" and mod.object in objDeleteList:
                    objDeleteList.remove(mod.object)  # remove it from deletion

    bpy.ops.object.select_all(action="DESELECT")
    for obj in objDeleteList:
        obj.select_set(True)
    bpy.ops.object.delete()


# Apply This Brush to the Canvas
def ApplyThisBrush(context, brush):
    for obj in context.scene.objects:
        if isCanvas(obj):
            for mod in obj.modifiers:
                if "BTool_" + brush.name in mod.name:
                    # Apply This Brush
                    context.view_layer.objects.active = obj
                    try:
                        bpy.ops.object.modifier_apply(modifier=mod.name)
                    except:  # if fails the means it is multiuser data
                        context.active_object.data = context.active_object.data.copy()  # so just make data unique
                        bpy.ops.object.modifier_apply(modifier=mod.name)
                    bpy.ops.object.select_all(action="TOGGLE")
                    bpy.ops.object.select_all(action="DESELECT")

    # Garbage Collector
    brush.select_set(True)
    # bpy.ops.object.delete()


# ------------------ Bool Tool OPERATORS --------------------------------------

# TODO
# class BTool_DrawPolyBrush(Operator):
#     bl_idname = "btool.draw_polybrush"
#     bl_label = "Draw Poly Brush"
#     bl_description = (
#         "Draw Polygonal Mask, can be applied to Canvas > Brush or Directly\n"
#         "Note: ESC to Cancel, Enter to Apply, Right Click to erase the Lines"
#     )

#     count = 0
#     store_cont_draw = False

#     @classmethod
#     def poll(cls, context):
#         return context.active_object is not None

#     def set_cont_draw(self, context, start=False):
#         # store / restore GP continuous drawing (see T52321)
#         scene = context.scene
#         tool_settings = scene.tool_settings
#         continuous = tool_settings.use_gpencil_continuous_drawing
#         if start:
#             self.store_cont_draw = continuous
#             tool_settings.use_gpencil_continuous_drawing = True
#         else:
#             tool_settings.use_gpencil_continuous_drawing = self.store_cont_draw

#     def modal(self, context, event):
#         self.count += 1
#         actObj = bpy.context.active_object
#         if self.count == 1:
#             actObj.select_set(True)
#             bpy.ops.gpencil.draw("INVOKE_DEFAULT", mode="DRAW_POLY")

#         if event.type == "RIGHTMOUSE":
#             # use this to pass to the Grease Pencil eraser (see T52321)
#             pass

#         if event.type in {"RET", "NUMPAD_ENTER"}:

#             bpy.ops.gpencil.convert(type="POLY")
#             self.set_cont_draw(context)

#             for obj in context.selected_objects:
#                 if obj.type == "CURVE":
#                     obj.name = "PolyDraw"
#                     bpy.context.view_layer.objects.active = obj
#                     bpy.ops.object.select_all(action="DESELECT")
#                     obj.select_set(True)
#                     bpy.ops.object.convert(target="MESH")
#                     bpy.ops.object.mode_set(mode="EDIT")
#                     bpy.ops.mesh.select_all(action="SELECT")
#                     bpy.ops.mesh.edge_face_add()
#                     bpy.ops.mesh.flip_normals()
#                     bpy.ops.object.mode_set(mode="OBJECT")
#                     bpy.ops.object.origin_set(type="ORIGIN_CENTER_OF_MASS")
#                     bpy.ops.object.modifier_add(type="SOLIDIFY")
#                     for mod in obj.modifiers:
#                         if mod.name == "Solidify":
#                             mod.name = "BTool_PolyBrush"
#                             mod.thickness = 1
#                             mod.offset = 0
#                     obj["BoolToolPolyBrush"] = True

#                     bpy.ops.object.select_all(action="DESELECT")
#                     bpy.context.view_layer.objects.active = actObj
#                     bpy.context.view_layer.update()
#                     actObj.select_set(True)
#                     obj.select_set(True)

#                     bpy.context.view_layer.grease_pencil.clear()
#                     bpy.ops.gpencil.data_unlink()

#             return {"FINISHED"}

#         if event.type == "ESC":
#             bpy.ops.ed.undo()  # remove o Grease Pencil
#             self.set_cont_draw(context)

#             self.report({"INFO"}, "Draw Poly Brush: Operation Cancelled by User")
#             return {"CANCELLED"}

#         return {"RUNNING_MODAL"}

#     def invoke(self, context, event):
#         if context.object:
#             self.set_cont_draw(context, start=True)
#             context.window_manager.modal_handler_add(self)
#             return {"RUNNING_MODAL"}
#         else:
#             self.report({"WARNING"}, "No active object, could not finish")
#             return {"CANCELLED"}


# Fast Transform
class BTool_FastTransform(Operator):
    bl_idname = "btool.fast_transform"
    bl_label = "Fast Transform"
    bl_description = "Enable Fast Transform"

    operator: StringProperty("")

    count = 0

    def modal(self, context, event):
        self.count += 1
        actObj = bpy.context.active_object
        useWire = bpy.context.preferences.addons[__name__].preferences.use_wire
        if self.count == 1:

            if isBrush(actObj) and actObj["BoolTool_FTransform"] == "True":
                EnableThisBrush(bpy.context, "False")
                if useWire:
                    actObj.display_type = "WIRE"
                else:
                    actObj.display_type = "BOUNDS"

            if self.operator == "Translate":
                bpy.ops.transform.translate("INVOKE_DEFAULT")
            if self.operator == "Rotate":
                bpy.ops.transform.rotate("INVOKE_DEFAULT")
            if self.operator == "Scale":
                bpy.ops.transform.resize("INVOKE_DEFAULT")

        if event.type == "LEFTMOUSE":
            if isBrush(actObj):
                EnableThisBrush(bpy.context, "True")
                actObj.display_type = "WIRE"
            return {"FINISHED"}

        if event.type in {"RIGHTMOUSE", "ESC"}:
            if isBrush(actObj):
                EnableThisBrush(bpy.context, "True")
                actObj.display_type = "WIRE"
            return {"CANCELLED"}

        return {"RUNNING_MODAL"}

    def invoke(self, context, event):
        if context.object:
            context.window_manager.modal_handler_add(self)
            return {"RUNNING_MODAL"}
        else:
            self.report({"WARNING"}, "No active object, could not finish")
            return {"CANCELLED"}


# -------------------  Bool Tool OPERATOR CLASSES --------------------------------------------------------


# Brush operators
# --------------------------------------------------------------------------------------


class BToolSetup():

    def execute(self, context):
        Operation(context, self.mode)
        return {"FINISHED"}

    def invoke(self, context, event):
        if len(context.selected_objects) < 2:
            self.report({"ERROR"}, "At least two objects must be selected")
            return {"CANCELLED"}

        return self.execute(context)


class BTool_Union(Operator, BToolSetup):
    bl_idname = "btool.boolean_union"
    bl_label = "Brush Union"
    bl_description = "This operator add a union brush to a canvas"
    bl_options = {"REGISTER", "UNDO"}

    mode = "UNION"


class BTool_Inters(Operator, BToolSetup):
    bl_idname = "btool.boolean_inters"
    bl_label = "Brush Intersection"
    bl_description = "This operator add a intersect brush to a canvas"
    bl_options = {"REGISTER", "UNDO"}

    mode = "INTERSECT"


class BTool_Diff(Operator, BToolSetup):
    bl_idname = "btool.boolean_diff"
    bl_label = "Brush Difference"
    bl_description = "This operator add a difference brush to a canvas"
    bl_options = {"REGISTER", "UNDO"}

    mode = "DIFFERENCE"


class BTool_Slice(Operator, BToolSetup):
    bl_idname = "btool.boolean_slice"
    bl_label = "Brush Slice"
    bl_description = "This operator add a intersect brush to a canvas"
    bl_options = {"REGISTER", "UNDO"}

    mode = "SLICE"


# Auto Boolean operators
# --------------------------------------------------------------------------------------


class Auto_Boolean:

    def objects_prepare(self):
        for ob in bpy.context.selected_objects:
            if ob.type != "MESH":
                ob.select_set(False)
        bpy.ops.object.make_single_user(object=True, obdata=True)
        bpy.ops.object.convert(target="MESH")

    def mesh_selection(self, ob, select_action):
        obj = bpy.context.active_object

        bpy.context.view_layer.objects.active = ob
        bpy.ops.object.mode_set(mode="EDIT")

        bpy.ops.mesh.reveal()
        bpy.ops.mesh.select_all(action=select_action)

        bpy.ops.object.mode_set(mode="OBJECT")
        bpy.context.view_layer.objects.active = obj

    def boolean_operation(self):
        obj = bpy.context.active_object
        obj.select_set(False)
        obs = bpy.context.selected_objects

        self.mesh_selection(obj, "DESELECT")

        for ob in obs:
            self.mesh_selection(ob, "SELECT")
            self.boolean_mod(obj, ob, self.mode)

        obj.select_set(True)

    def boolean_mod(self, obj, ob, mode, ob_delete=True):
        md = obj.modifiers.new("Auto Boolean", "BOOLEAN")
        md.show_viewport = False
        md.operation = mode
        md.object = ob

        override = {"object": obj}
        bpy.ops.object.modifier_apply(override, modifier=md.name)

        if ob_delete:
            bpy.data.objects.remove(ob)

    def execute(self, context):
        self.objects_prepare()
        self.boolean_operation()
        return {"FINISHED"}

    def invoke(self, context, event):
        if len(context.selected_objects) < 2:
            self.report({"ERROR"}, "At least two objects must be selected")
            return {"CANCELLED"}

        return self.execute(context)


class OBJECT_OT_BoolTool_Auto_Union(Operator, Auto_Boolean):
    bl_idname = "object.booltool_auto_union"
    bl_label = "Bool Tool Union"
    bl_description = "Combine selected objects"
    bl_options = {"REGISTER", "UNDO"}

    mode = "UNION"


class OBJECT_OT_BoolTool_Auto_Difference(Operator, Auto_Boolean):
    bl_idname = "object.booltool_auto_difference"
    bl_label = "Bool Tool Difference"
    bl_description = "Subtract selected objects from active object"
    bl_options = {"REGISTER", "UNDO"}

    mode = "DIFFERENCE"


class OBJECT_OT_BoolTool_Auto_Intersect(Operator, Auto_Boolean):
    bl_idname = "object.booltool_auto_intersect"
    bl_label = "Bool Tool Intersect"
    bl_description = "Keep only intersecting geometry"
    bl_options = {"REGISTER", "UNDO"}

    mode = "INTERSECT"


class OBJECT_OT_BoolTool_Auto_Slice(Operator, Auto_Boolean):
    bl_idname = "object.booltool_auto_slice"
    bl_label = "Bool Tool Slice"
    bl_description = "Slice active object along the selected objects"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        space_data = context.space_data
        is_local_view = bool(space_data.local_view)
        self.objects_prepare()

        ob1 = context.active_object
        ob1.select_set(False)
        self.mesh_selection(ob1, "DESELECT")

        for ob2 in context.selected_objects:

            self.mesh_selection(ob2, "SELECT")

            ob1_copy = ob1.copy()
            ob1_copy.data = ob1.data.copy()

            for coll in ob1.users_collection:
                coll.objects.link(ob1_copy)

            if is_local_view:
                ob1_copy.local_view_set(space_data, True)

            self.boolean_mod(ob1, ob2, "DIFFERENCE", ob_delete=False)
            self.boolean_mod(ob1_copy, ob2, "INTERSECT")
            ob1_copy.select_set(True)

        context.view_layer.objects.active = ob1_copy

        return {"FINISHED"}


# Utils Class ---------------------------------------------------------------

# Find the Brush Selected in Three View
class BTool_FindBrush(Operator):
    bl_idname = "btool.find_brush"
    bl_label = ""
    bl_description = "Find the selected brush"

    obj: StringProperty("")

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        for ob in bpy.context.view_layer.objects:
            if ob.name == self.obj:
                bpy.ops.object.select_all(action="TOGGLE")
                bpy.ops.object.select_all(action="DESELECT")
                bpy.context.view_layer.objects.active = ob
                ob.set_select(state=True)
        return {"FINISHED"}


# Move The Modifier in The Stack Up or Down
class BTool_MoveStack(Operator):
    bl_idname = "btool.move_stack"
    bl_label = ""
    bl_description = "Move this Brush Up/Down in the Stack"

    modif: StringProperty("")
    direction: StringProperty("")

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        if self.direction == "UP":
            bpy.ops.object.modifier_move_up(modifier=self.modif)
        if self.direction == "DOWN":
            bpy.ops.object.modifier_move_down(modifier=self.modif)
        return {"FINISHED"}


# Enable or Disable a Brush in the Three View
class BTool_EnableBrush(Operator):
    bl_idname = "btool.enable_brush"
    bl_label = ""
    bl_description = "Removes all BoolTool config assigned to it"

    thisObj: StringProperty("")

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        # in this case is just one object but the function accept more than one at once
        EnableBrush(context, [self.thisObj], context.active_object)
        return {"FINISHED"}


# Enable or Disable a Brush Directly
class BTool_EnableThisBrush(Operator):
    bl_idname = "btool.enable_this_brush"
    bl_label = ""
    bl_description = "Toggles this brush"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        EnableThisBrush(context, "None")
        return {"FINISHED"}


# Enable or Disable a Brush Directly
class BTool_EnableFTransform(Operator):
    bl_idname = "btool.enable_ftransf"
    bl_label = ""
    bl_description = "Use Fast Transformations to improve speed"

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        EnableFTransf(context)
        return {"FINISHED"}


# Other Operations -------------------------------------------------------

# Remove a Brush or a Canvas
class BTool_Remove(Operator):
    bl_idname = "btool.remove"
    bl_label = "Bool Tool Remove"
    bl_description = "Removes all BoolTool config assigned to it"
    bl_options = {"UNDO"}

    thisObj: StringProperty("")
    Prop: StringProperty("")

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        Remove(context, self.thisObj, self.Prop)
        return {"FINISHED"}


# Apply All to Canvas
class BTool_AllBrushToMesh(Operator):
    bl_idname = "btool.to_mesh"
    bl_label = "Apply All Canvas"
    bl_description = "Apply all brushes of this canvas"
    bl_options = {"UNDO"}

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def execute(self, context):
        lists = bpy.context.selected_objects
        ApplyAll(context, lists)
        return {"FINISHED"}


# Apply This Brush to the Canvas
class BTool_BrushToMesh(Operator):
    bl_idname = "btool.brush_to_mesh"
    bl_label = "Apply this Brush to Canvas"
    bl_description = "Apply this brush to the canvas"
    bl_options = {"UNDO"}

    @classmethod
    def poll(cls, context):

        if isBrush(context.active_object):
            return True
        else:
            return False

    def execute(self, context):
        ApplyThisBrush(context, bpy.context.active_object)
        return {"FINISHED"}


# TODO
# Apply This Brush To Mesh


# ------------------- MENU CLASSES ------------------------------

# 3Dview Header Menu
class VIEW3D_MT_booltool_menu(Menu):
    bl_label = "Bool Tool"
    bl_idname = "VIEW3D_MT_booltool_menu"

    def draw(self, context):
        layout = self.layout

        layout.label(text="Auto Boolean")
        layout.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
        layout.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
        layout.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
        layout.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")

        layout.separator()

        layout.label(text="Brush Boolean")
        layout.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
        layout.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
        layout.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
        layout.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")

        if isCanvas(context.active_object):
            layout.separator()
            layout.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")
            Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
            Rem.thisObj = ""
            Rem.Prop = "CANVAS"

        if isBrush(context.active_object):
            layout.separator()
            layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
            Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
            Rem.thisObj = ""
            Rem.Prop = "BRUSH"


def VIEW3D_BoolTool_Menu(self, context):
    self.layout.menu(VIEW3D_MT_booltool_menu.bl_idname)


# ---------------- Toolshelf: Tools ---------------------


class VIEW3D_PT_booltool_tools(Panel):
    bl_category = "objectmode"
    bl_label = "Bool Tool"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return context.active_object is not None

    def draw(self, context):
        layout = self.layout

        col = layout.column(align=True)
        col.label(text="Auto Boolean")
        col.operator(OBJECT_OT_BoolTool_Auto_Difference.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
        col.operator(OBJECT_OT_BoolTool_Auto_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
        col.operator(OBJECT_OT_BoolTool_Auto_Intersect.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
        col.operator(OBJECT_OT_BoolTool_Auto_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")

        col = layout.column(align=True)
        col.label(text="Brush Boolean")
        col.operator(BTool_Diff.bl_idname, text="Difference", icon="SELECT_SUBTRACT")
        col.operator(BTool_Union.bl_idname, text="Union", icon="SELECT_EXTEND")
        col.operator(BTool_Inters.bl_idname, text="Intersect", icon="SELECT_INTERSECT")
        col.operator(BTool_Slice.bl_idname, text="Slice", icon="SELECT_DIFFERENCE")

        # TODO Draw Poly Brush
        # main.separator()

        # col = main.column(align=True)
        # col.label(text="Draw:", icon="MESH_CUBE")
        # col.separator()
        # col.operator(BTool_DrawPolyBrush.bl_idname, icon="LINE_DATA")


# ---------- Toolshelf: Properties --------------------------------------------------------


class VIEW3D_PT_booltool_config(Panel):
    bl_category = "objectmode"
    bl_label = "Properties"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"
    bl_parent_id = "VIEW3D_PT_booltool_tools"

    @classmethod
    def poll(cls, context):
        actObj = context.active_object
        return isCanvas(actObj) or isBrush(actObj)  # or isPolyBrush(actObj)

    def draw(self, context):
        layout = self.layout
        actObj = context.active_object

        row = layout.row(align=True)

        if isCanvas(actObj):

            row.label(text="CANVAS", icon="MESH_GRID")
            row = layout.row()
            row.prop(context.scene, "BoolHide", text="Hide Bool objects")
            row = layout.row(align=True)
            row.operator(BTool_AllBrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply All")

            row = layout.row(align=True)
            Rem = row.operator(BTool_Remove.bl_idname, icon="X", text="Remove All")
            Rem.thisObj = ""
            Rem.Prop = "CANVAS"

            if isBrush(actObj):
                layout.separator()

        if isBrush(actObj):

            if actObj["BoolToolBrush"] == "DIFFERENCE":
                icon = "SELECT_SUBTRACT"
            elif actObj["BoolToolBrush"] == "UNION":
                icon = "SELECT_EXTEND"
            elif actObj["BoolToolBrush"] == "INTERSECT":
                icon = "SELECT_INTERSECT"
            elif actObj["BoolToolBrush"] == "SLICE":
                icon = "SELECT_DIFFERENCE"

            row.label(text="BRUSH", icon=icon)

            if actObj["BoolTool_FTransform"] == "True":
                icon = "PMARKER_ACT"
            else:
                icon = "PMARKER"
            if isFTransf():
                pass

            if isFTransf():
                row = layout.row(align=True)
                row.operator(BTool_EnableFTransform.bl_idname, text="Fast Vis", icon=icon)
                row.operator(BTool_EnableThisBrush.bl_idname, text="Enable", icon="HIDE_OFF")
            else:
                row.operator(BTool_EnableThisBrush.bl_idname, icon="HIDE_OFF")

            layout.operator(BTool_BrushToMesh.bl_idname, icon="MOD_LATTICE", text="Apply Brush")
            Rem = layout.operator(BTool_Remove.bl_idname, icon="X", text="Remove Brush")
            Rem.thisObj = ""
            Rem.Prop = "BRUSH"

        # TODO
        # if isPolyBrush(actObj):
        #     layout.label(text="POLY BRUSH", icon="LINE_DATA")
        #     mod = actObj.modifiers["BTool_PolyBrush"]
        #     layout.prop(mod, "thickness", text="Size")


# ---------- Toolshelf: Brush Viewer -------------------------------------------------------


class VIEW3D_PT_booltool_bviewer(Panel):
    bl_category = "objectmode"
    bl_label = "Brush Viewer"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = "objectmode"
    bl_parent_id = "VIEW3D_PT_booltool_tools"

    @classmethod
    def poll(cls, context):
        actObj = bpy.context.active_object

        if isCanvas(actObj):
            return True
        else:
            return False

    def draw(self, context):

        actObj = bpy.context.active_object

        if isCanvas(actObj):

            for mod in actObj.modifiers:
                container = self.layout.box()
                row = container.row(align=True)

                if "BTool_" in mod.name:

                    if mod.operation == "DIFFERENCE":
                        icon = "SELECT_SUBTRACT"
                    elif mod.operation == "UNION":
                        icon = "SELECT_EXTEND"
                    elif mod.operation == "INTERSECT":
                        icon = "SELECT_INTERSECT"
                    elif mod.operation == "SLICE":
                        icon = "SELECT_DIFFERENCE"

                    objSelect = row.operator("btool.find_brush", text=mod.object.name, icon=icon, emboss=False)
                    objSelect.obj = mod.object.name

                    EnableIcon = "RESTRICT_VIEW_ON"
                    if mod.show_viewport:
                        EnableIcon = "RESTRICT_VIEW_OFF"
                    Enable = row.operator(BTool_EnableBrush.bl_idname, icon=EnableIcon, emboss=False)
                    Enable.thisObj = mod.object.name

                    Remove = row.operator("btool.remove", text="", icon="X", emboss=False)
                    Remove.thisObj = mod.object.name
                    Remove.Prop = "THIS"

                else:
                    row.label(text=mod.name)

                Up = row.operator("btool.move_stack", icon="TRIA_UP", emboss=False)
                Up.modif = mod.name
                Up.direction = "UP"

                Dw = row.operator("btool.move_stack", icon="TRIA_DOWN", emboss=False)
                Dw.modif = mod.name
                Dw.direction = "DOWN"


# ------------------ BOOL TOOL ADD-ON PREFERENCES ----------------------------


shortcut_list = (
    ("3D View", None),
    ("Menu", "Ctrl Shift B"),

    ("Auto Operators", None),
    ("Difference", "Ctrl Shift Num -"),
    ("Union", "Ctrl Shift Num +"),
    ("Intersect", "Ctrl Shift Num *"),
    ("Slice", "Ctrl Shift Num /"),

    ("Brush Operators", None),
    ("Difference", "Ctrl Num -"),
    ("Union", "Ctrl Num +"),
    ("Intersect", "Ctrl Num *"),
    ("Slice", "Ctrl Num /"),
    ("Brush To Mesh", "Ctrl Num Enter"),
    ("All Brushes To Mesh", "Ctrl Shift Num Enter"),
)


def UpdateBoolTool_Pref(self, context):
    if self.fast_transform:
        RegisterFastT()
    else:
        UnRegisterFastT()


# Define Panel classes for updating
panels = (
    VIEW3D_PT_booltool_tools,
    VIEW3D_PT_booltool_config,
    VIEW3D_PT_booltool_bviewer,
)


def update_panels(self, context):
    try:
        for panel in panels:
            if "bl_rna" in panel.__dict__:
                bpy.utils.unregister_class(panel)

        for panel in panels:
            panel.bl_category = context.preferences.addons[
                __name__
            ].preferences.category
            bpy.utils.register_class(panel)

    except Exception as e:
        message = "Bool Tool: Updating Panel locations has failed"
        print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))


def icon_tria(prop):
    if prop:
        return "TRIA_DOWN"
    return "TRIA_RIGHT"


class PREFS_BoolTool_Props(AddonPreferences):
    bl_idname = __name__

    fast_transform: BoolProperty(
        name="Fast Transformations",
        update=UpdateBoolTool_Pref,
        description="Replace the Transform HotKeys (G,R,S)\n"
        "for a custom version that can optimize the visualization of Brushes",
    )
    use_wire: BoolProperty(
        name="Display As Wireframe",
        description="Display brush as wireframe instead of bounding box",
    )
    category: StringProperty(
        name="Tab Name",
        description="Set sidebar tab name",
        default="Edit",
        update=update_panels,
    )
    show_shortcuts: BoolProperty(name="Shortcuts")

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        layout.use_property_decorate = False

        col = layout.column()
        col.prop(self, "category")
        col.prop(self, "fast_transform")
        col.prop(self, "use_wire")

        col = layout.column()
        col.scale_y = 1.2
        col.use_property_split = False
        col.prop(self, "show_shortcuts", icon=icon_tria(self.show_shortcuts))

        if self.show_shortcuts:

            col = layout.column()

            for key_name, key_comb in shortcut_list:
                if key_comb is None:
                    col.separator()
                    col.label(text=key_name)
                else:
                    row = col.row(align=True)
                    row.scale_y = 0.7
                    row.box().label(text=key_name)
                    row.box().label(text=key_comb)


# ------------------- Class List ------------------------------------------------

classes = (
    PREFS_BoolTool_Props,
    VIEW3D_MT_booltool_menu,
    VIEW3D_PT_booltool_tools,
    VIEW3D_PT_booltool_config,
    VIEW3D_PT_booltool_bviewer,
    OBJECT_OT_BoolTool_Auto_Union,
    OBJECT_OT_BoolTool_Auto_Difference,
    OBJECT_OT_BoolTool_Auto_Intersect,
    OBJECT_OT_BoolTool_Auto_Slice,
    BTool_Union,
    BTool_Diff,
    BTool_Inters,
    BTool_Slice,
    # TODO Draw Poly Brush
    # BTool_DrawPolyBrush,
    BTool_Remove,
    BTool_AllBrushToMesh,
    BTool_BrushToMesh,
    BTool_FindBrush,
    BTool_MoveStack,
    BTool_EnableBrush,
    BTool_EnableThisBrush,
    BTool_EnableFTransform,
    BTool_FastTransform,
)


# ------------------- REGISTER ------------------------------------------------

addon_keymaps = []
addon_keymapsFastT = []


# Fast Transform HotKeys Register
def RegisterFastT():
    wm = bpy.context.window_manager
    km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")

    kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "G", "PRESS")
    kmi.properties.operator = "Translate"
    addon_keymapsFastT.append((km, kmi))

    kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "R", "PRESS")
    kmi.properties.operator = "Rotate"
    addon_keymapsFastT.append((km, kmi))

    kmi = km.keymap_items.new(BTool_FastTransform.bl_idname, "S", "PRESS")
    kmi.properties.operator = "Scale"
    addon_keymapsFastT.append((km, kmi))


# Fast Transform HotKeys UnRegister
def UnRegisterFastT():
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        for km, kmi in addon_keymapsFastT:
            km.keymap_items.remove(kmi)

    addon_keymapsFastT.clear()


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    update_panels(None, bpy.context)

    # Scene variables
    bpy.types.Scene.BoolHide = BoolProperty(
        default=False,
        description="Hide boolean objects",
        update=update_BoolHide,
    )
    bpy.types.VIEW3D_MT_object.append(VIEW3D_BoolTool_Menu)

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon

    # create the boolean menu hotkey
    if kc is not None:
        km = kc.keymaps.new(name="Object Mode")

        kmi = km.keymap_items.new("wm.call_menu", "B", "PRESS", ctrl=True, shift=True)
        kmi.properties.name = "VIEW3D_MT_booltool_menu"
        addon_keymaps.append((km, kmi))

        # Brush Operators
        kmi = km.keymap_items.new(BTool_Union.bl_idname, "NUMPAD_PLUS", "PRESS", ctrl=True)
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(BTool_Diff.bl_idname, "NUMPAD_MINUS", "PRESS", ctrl=True)
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(BTool_Inters.bl_idname, "NUMPAD_ASTERIX", "PRESS", ctrl=True)
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(BTool_Slice.bl_idname, "NUMPAD_SLASH", "PRESS", ctrl=True)
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(BTool_BrushToMesh.bl_idname, "NUMPAD_ENTER", "PRESS", ctrl=True)
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(
            BTool_AllBrushToMesh.bl_idname,
            "NUMPAD_ENTER",
            "PRESS",
            ctrl=True,
            shift=True,
        )
        addon_keymaps.append((km, kmi))

        # Auto Operators
        kmi = km.keymap_items.new(
            OBJECT_OT_BoolTool_Auto_Union.bl_idname,
            "NUMPAD_PLUS",
            "PRESS",
            ctrl=True,
            shift=True,
        )
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(
            OBJECT_OT_BoolTool_Auto_Difference.bl_idname,
            "NUMPAD_MINUS",
            "PRESS",
            ctrl=True,
            shift=True,
        )
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(
            OBJECT_OT_BoolTool_Auto_Intersect.bl_idname,
            "NUMPAD_ASTERIX",
            "PRESS",
            ctrl=True,
            shift=True,
        )
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(
            OBJECT_OT_BoolTool_Auto_Slice.bl_idname,
            "NUMPAD_SLASH",
            "PRESS",
            ctrl=True,
            shift=True,
        )
        addon_keymaps.append((km, kmi))


def unregister():
    # Keymapping
    # remove keymaps when add-on is deactivated
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)

    addon_keymaps.clear()
    UnRegisterFastT()
    bpy.types.VIEW3D_MT_object.remove(VIEW3D_BoolTool_Menu)
    del bpy.types.Scene.BoolHide

    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()