Skip to content
Snippets Groups Projects
qcd_operators.py 24.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    # Copyright 2011, Ryan Inch
    
    import bpy
    
    from bpy.types import (
        Operator,
    )
    
    from bpy.props import (
        BoolProperty,
        StringProperty,
        IntProperty
    )
    
    
    from .internals import (
        update_property_group,
    
        generate_state,
    
        get_move_active,
        update_qcd_header,
    
    from .operator_utils import (
    
        mode_converter,
    
        apply_to_children,
    
        select_collection_objects,
    
        set_exclude_state,
    
        isolate_sel_objs_collections,
        disable_sel_objs_collections,
    
    class LockedObjects():
        def __init__(self):
            self.objs = []
            self.mode = ""
    
    def get_locked_objs(context):
        # get objects not in object mode
        locked = LockedObjects()
        if context.mode == 'OBJECT':
            return locked
    
        if context.view_layer.objects.active:
            active = context.view_layer.objects.active
            locked.mode = mode_converter[context.mode]
    
            for obj in context.view_layer.objects:
                if obj.mode != 'OBJECT':
                    if obj.mode not in ['POSE', 'WEIGHT_PAINT'] or obj == active:
                        if obj.mode == active.mode:
                            locked.objs.append(obj)
    
        return locked
    
    
    
    class QCDAllBase():
        meta_op = False
        meta_report = None
    
        context = None
        view_layer = ""
        history = None
        orig_active_collection = None
        orig_active_object = None
        locked = None
    
        @classmethod
        def init(cls, context):
            cls.context = context
            cls.orig_active_collection = context.view_layer.active_layer_collection
            cls.view_layer = context.view_layer.name
            cls.orig_active_object = context.view_layer.objects.active
    
    
            if not cls.view_layer in internals.qcd_history:
                internals.qcd_history[cls.view_layer] = []
    
            cls.history = internals.qcd_history[cls.view_layer]
    
    
            cls.locked = get_locked_objs(context)
    
        @classmethod
        def apply_history(cls):
    
            for x, item in enumerate(internals.layer_collections.values()):
    
                item["ptr"].exclude = cls.history[x]
    
            # clear rto history
    
            del internals.qcd_history[cls.view_layer]
    
    
            internals.qcd_collection_state.clear()
            cls.history = None
    
        @classmethod
        def finalize(cls):
            # restore active collection
            cls.context.view_layer.active_layer_collection = cls.orig_active_collection
    
            # restore active object if possible
            if cls.orig_active_object:
                if cls.orig_active_object.name in cls.context.view_layer.objects:
                    cls.context.view_layer.objects.active = cls.orig_active_object
    
            # restore locked objects back to their original mode
            # needed because of exclude child updates
            if cls.context.view_layer.objects.active:
                if cls.locked.objs:
                    bpy.ops.object.mode_set(mode=cls.locked.mode)
    
        @classmethod
        def clear(cls):
            cls.context = None
            cls.view_layer = ""
            cls.history = None
            cls.orig_active_collection = None
            cls.orig_active_object = None
    
    
    
    class EnableAllQCDSlotsMeta(Operator):
        '''QCD All Meta Operator'''
        bl_label = "Quick View Toggles"
        bl_idname = "view3d.enable_all_qcd_slots_meta"
    
    
        @classmethod
        def description(cls, context, properties):
            hotkey_string = (
                "  * LMB - Enable all slots/Restore.\n"
    
                "  * Alt+LMB - Discard History.\n"
    
                "  * LMB+Hold - Menu"
                )
    
            return hotkey_string
    
    
        def invoke(self, context, event):
            qab = QCDAllBase
    
            modifiers = get_modifiers(event)
    
            qab.meta_op = True
    
            if modifiers == {"alt"}:
    
                bpy.ops.view3d.discard_qcd_history()
    
    
            else:
                qab.init(context)
    
                if not qab.history:
                    bpy.ops.view3d.enable_all_qcd_slots()
    
                else:
                    qab.apply_history()
                    qab.finalize()
    
    
            if qab.meta_report:
                self.report({"INFO"}, qab.meta_report)
                qab.meta_report = None
    
            qab.meta_op = False
    
    
            return {'FINISHED'}
    
    
    class EnableAllQCDSlots(Operator):
        '''Toggles between the current state and all enabled'''
        bl_label = "Enable All QCD Slots"
        bl_idname = "view3d.enable_all_qcd_slots"
        bl_options = {'REGISTER', 'UNDO'}
    
        def execute(self, context):
            qab = QCDAllBase
    
            # validate qcd slots
    
            if not dict(internals.qcd_slots):
    
                if qab.meta_op:
                    qab.meta_report = "No QCD slots."
                else:
                    self.report({"INFO"}, "No QCD slots.")
    
                return {'CANCELLED'}
    
            qab.init(context)
    
            if not qab.history:
                keep_history = False
    
    
                for laycol in internals.layer_collections.values():
                    is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
    
    
                    qab.history.append(laycol["ptr"].exclude)
    
                    if is_qcd_slot and laycol["ptr"].exclude:
                        keep_history = True
                        set_exclude_state(laycol["ptr"], False)
    
    
                if not keep_history:
                    # clear rto history
    
                    del internals.qcd_history[qab.view_layer]
    
                    qab.clear()
    
                    if qab.meta_op:
                        qab.meta_report = "All QCD slots are already enabled."
    
                    else:
                        self.report({"INFO"}, "All QCD slots are already enabled.")
    
                    return {'CANCELLED'}
    
                internals.qcd_collection_state.clear()
                internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            else:
                qab.apply_history()
    
            qab.finalize()
    
            return {'FINISHED'}
    
    
    class EnableAllQCDSlotsIsolated(Operator):
        '''Toggles between the current state and all enabled (non-QCD collections disabled)'''
        bl_label = "Enable All QCD Slots Isolated"
        bl_idname = "view3d.enable_all_qcd_slots_isolated"
        bl_options = {'REGISTER', 'UNDO'}
    
        def execute(self, context):
            qab = QCDAllBase
    
            # validate qcd slots
    
            if not dict(internals.qcd_slots):
    
                self.report({"INFO"}, "No QCD slots.")
    
                return {'CANCELLED'}
    
            qab.init(context)
    
    
            if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
    
                # clear rto history
    
                del internals.qcd_history[qab.view_layer]
    
                qab.clear()
    
                self.report({"WARNING"}, "Cannot execute.  The active object would be lost.")
    
                return {'CANCELLED'}
    
            if not qab.history:
                keep_history = False
    
    
                for laycol in internals.layer_collections.values():
                    is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
    
    
                    qab.history.append(laycol["ptr"].exclude)
    
                    if is_qcd_slot and laycol["ptr"].exclude:
                        keep_history = True
                        set_exclude_state(laycol["ptr"], False)
    
                    if not is_qcd_slot and not laycol["ptr"].exclude:
                        keep_history = True
                        set_exclude_state(laycol["ptr"], True)
    
    
                if not keep_history:
                    # clear rto history
    
                    del internals.qcd_history[qab.view_layer]
    
                    qab.clear()
    
                    self.report({"INFO"}, "All QCD slots are already enabled and isolated.")
                    return {'CANCELLED'}
    
                internals.qcd_collection_state.clear()
                internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            else:
                qab.apply_history()
    
            qab.finalize()
    
            return {'FINISHED'}
    
    
    class IsolateSelectedObjectsCollections(Operator):
        '''Isolate collections (via EC) that contain the selected objects'''
        bl_label = "Isolate Selected Objects Collections"
        bl_idname = "view3d.isolate_selected_objects_collections"
    
        def execute(self, context):
            qab = QCDAllBase
            qab.init(context)
    
            use_active = bool(context.mode != 'OBJECT')
    
            # isolate
            error = isolate_sel_objs_collections(qab.view_layer, "exclude", "QCD", use_active=use_active)
    
            if error:
                qab.clear()
                self.report({"WARNING"}, error)
                return {'CANCELLED'}
    
            qab.finalize()
    
            internals.qcd_collection_state.clear()
            internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            return {'FINISHED'}
    
    
    class DisableSelectedObjectsCollections(Operator):
        '''Disable all collections that contain the selected objects'''
        bl_label = "Disable Selected Objects Collections"
        bl_idname = "view3d.disable_selected_objects_collections"
    
        def execute(self, context):
            qab = QCDAllBase
            qab.init(context)
    
            if qab.locked.objs:
                # clear rto history
                del internals.qcd_history[qab.view_layer]
                qab.clear()
    
                self.report({"WARNING"}, "Can only be executed in Object Mode")
                return {'CANCELLED'}
    
            # disable
            error = disable_sel_objs_collections(qab.view_layer, "exclude", "QCD")
    
            if error:
                qab.clear()
                self.report({"WARNING"}, error)
                return {'CANCELLED'}
    
            qab.finalize()
    
            internals.qcd_collection_state.clear()
            internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            return {'FINISHED'}
    
    
    
    class DisableAllNonQCDSlots(Operator):
        '''Toggles between the current state and all non-QCD collections disabled'''
        bl_label = "Disable All Non QCD Slots"
        bl_idname = "view3d.disable_all_non_qcd_slots"
        bl_options = {'REGISTER', 'UNDO'}
    
    
        def execute(self, context):
            qab = QCDAllBase
    
            # validate qcd slots
    
            if not dict(internals.qcd_slots):
    
                self.report({"INFO"}, "No QCD slots.")
    
                return {'CANCELLED'}
    
            qab.init(context)
    
    
            if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
    
                # clear rto history
    
                del internals.qcd_history[qab.view_layer]
    
                qab.clear()
    
                self.report({"WARNING"}, "Cannot execute.  The active object would be lost.")
    
                return {'CANCELLED'}
    
            if not qab.history:
                keep_history = False
    
    
                for laycol in internals.layer_collections.values():
                    is_qcd_slot = internals.qcd_slots.contains(name=laycol["name"])
    
    
                    qab.history.append(laycol["ptr"].exclude)
    
                    if not is_qcd_slot and not laycol["ptr"].exclude:
                        keep_history = True
                        set_exclude_state(laycol["ptr"], True)
    
                if not keep_history:
                    # clear rto history
    
                    del internals.qcd_history[qab.view_layer]
    
                    qab.clear()
    
                    self.report({"INFO"}, "All non QCD slots are already disabled.")
                    return {'CANCELLED'}
    
                internals.qcd_collection_state.clear()
                internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            else:
                qab.apply_history()
    
            qab.finalize()
    
            return {'FINISHED'}
    
    
    class DisableAllCollections(Operator):
        '''Toggles between the current state and all collections disabled'''
        bl_label = "Disable All collections"
        bl_idname = "view3d.disable_all_collections"
        bl_options = {'REGISTER', 'UNDO'}
    
    
        def execute(self, context):
            qab = QCDAllBase
    
            qab.init(context)
    
            if qab.locked.objs:
                # clear rto history
    
                del internals.qcd_history[qab.view_layer]
    
                qab.clear()
    
                self.report({"WARNING"}, "Cannot execute.  The active object would be lost.")
    
                return {'CANCELLED'}
    
            if not qab.history:
    
                for laycol in internals.layer_collections.values():
    
    
                    qab.history.append(laycol["ptr"].exclude)
    
                if all(qab.history): # no collections are enabled
                    # clear rto history
    
                    del internals.qcd_history[qab.view_layer]
    
                    qab.clear()
    
                    self.report({"INFO"}, "All collections are already disabled.")
                    return {'CANCELLED'}
    
    
                for laycol in internals.layer_collections.values():
    
                    laycol["ptr"].exclude = True
    
                internals.qcd_collection_state.clear()
                internals.qcd_collection_state.update(internals.generate_state(qcd=True))
    
            else:
                qab.apply_history()
    
            qab.finalize()
    
            return {'FINISHED'}
    
    
    class SelectAllQCDObjects(Operator):
        '''Select all objects in QCD slots'''
        bl_label = "Select All QCD Objects"
        bl_idname = "view3d.select_all_qcd_objects"
        bl_options = {'REGISTER', 'UNDO'}
    
    
        def execute(self, context):
            qab = QCDAllBase
    
            if context.mode != 'OBJECT':
    
                self.report({"WARNING"}, "Can only be executed in Object Mode")
    
                return {'CANCELLED'}
    
            if not context.selectable_objects:
                if qab.meta_op:
                    qab.meta_report = "No objects present to select."
    
                else:
                    self.report({"INFO"}, "No objects present to select.")
    
                return {'CANCELLED'}
    
            orig_selected_objects = context.selected_objects
    
            bpy.ops.object.select_all(action='DESELECT')
    
    
            for slot, collection_name in internals.qcd_slots:
    
                select_collection_objects(
                    is_master_collection=False,
                    collection_name=collection_name,
                    replace=False,
                    nested=False,
                    selection_state=True
                    )
    
            if context.selected_objects == orig_selected_objects:
    
                for slot, collection_name in internals.qcd_slots:
    
                    select_collection_objects(
                        is_master_collection=False,
                        collection_name=collection_name,
                        replace=False,
                        nested=False,
                        selection_state=False
                        )
    
    
            return {'FINISHED'}
    
    
    class DiscardQCDHistory(Operator):
        '''Discard QCD History'''
        bl_label = "Discard History"
        bl_idname = "view3d.discard_qcd_history"
    
        def execute(self, context):
            qab = QCDAllBase
    
            view_layer = context.view_layer.name
    
    
            if view_layer in internals.qcd_history:
                del internals.qcd_history[view_layer]
    
            # update header UI
            update_qcd_header()
    
    
            return {'FINISHED'}
    
    
    
    class MoveToQCDSlot(Operator):
        '''Move object(s) to QCD slot'''
        bl_label = "Move To QCD Slot"
        bl_idname = "view3d.move_to_qcd_slot"
        bl_options = {'REGISTER', 'UNDO'}
    
        slot: StringProperty()
        toggle: BoolProperty()
    
        def execute(self, context):
    
            selected_objects = get_move_selection()
            active_object = get_move_active()
    
            internals.move_triggered = True
    
    
            qcd_laycol = None
    
            slot_name = internals.qcd_slots.get_name(self.slot)
    
                qcd_laycol = internals.layer_collections[slot_name]["ptr"]
    
    
            else:
                return {'CANCELLED'}
    
    
            if not selected_objects:
                return {'CANCELLED'}
    
            # adds object to slot
            if self.toggle:
                if not active_object:
    
                    active_object = tuple(selected_objects)[0]
    
    
                if not active_object.name in qcd_laycol.collection.objects:
                    for obj in selected_objects:
                        if obj.name not in qcd_laycol.collection.objects:
                            qcd_laycol.collection.objects.link(obj)
    
                else:
                    for obj in selected_objects:
                        if obj.name in qcd_laycol.collection.objects:
    
                            if len(obj.users_collection) == 1:
                                continue
    
                            qcd_laycol.collection.objects.unlink(obj)
    
    
            # moves object to slot
            else:
                for obj in selected_objects:
                    if obj.name not in qcd_laycol.collection.objects:
                        qcd_laycol.collection.objects.link(obj)
    
                    for collection in obj.users_collection:
    
                        qcd_idx = internals.qcd_slots.get_idx(collection.name)
    
                        if qcd_idx != self.slot:
                            collection.objects.unlink(obj)
    
    
    
            # update the active object if needed
    
            if not context.active_object:
                try:
                    context.view_layer.objects.active = active_object
    
    
                except RuntimeError: # object not in visible slot
    
    
            return {'FINISHED'}
    
    
    class ViewMoveQCDSlot(Operator):
    
        bl_idname = "view3d.view_move_qcd_slot"
        bl_options = {'REGISTER', 'UNDO'}
    
        slot: StringProperty()
    
    
        @classmethod
        def description(cls, context, properties):
    
            slot_name = internals.qcd_slots.get_name(properties.slot)
    
            slot_string = f"QCD Slot {properties.slot}: \"{slot_name}\"\n"
    
            selection_hotkeys = ""
    
            if context.mode == 'OBJECT':
                selection_hotkeys = (
                    ".\n"
                    "  * Alt+LMB - Select objects in slot.\n"
                    "  * Alt+Shift+LMB - Toggle objects' selection for slot"
                    )
    
    
                "  * LMB - Isolate slot.\n"
                "  * Shift+LMB - Toggle slot.\n"
                "  * Ctrl+LMB - Move objects to slot.\n"
    
                "  * Ctrl+Shift+LMB - Toggle objects' slot"
                + selection_hotkeys
    
                )
    
            return f"{slot_string}{hotkey_string}"
    
    
        def invoke(self, context, event):
            modifiers = get_modifiers(event)
    
            if modifiers == {"shift"}:
                bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=True)
    
            elif modifiers == {"ctrl"}:
                bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=False)
    
            elif modifiers == {"ctrl", "shift"}:
                bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=True)
    
    
            elif modifiers == {"alt"}:
                select_collection_objects(
    
                    is_master_collection=False,
    
                    collection_name=internals.qcd_slots.get_name(self.slot),
    
                    replace=True,
                    nested=False
                    )
    
            elif modifiers == {"alt", "shift"}:
                select_collection_objects(
    
                    is_master_collection=False,
    
                    collection_name=internals.qcd_slots.get_name(self.slot),
    
                    replace=False,
                    nested=False
                    )
    
    
            else:
                bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False)
    
    
    class ViewQCDSlot(Operator):
        '''View objects in QCD slot'''
        bl_label = "View QCD Slot"
        bl_idname = "view3d.view_qcd_slot"
    
        bl_options = {'UNDO'}
    
    
        slot: StringProperty()
        toggle: BoolProperty()
    
        def execute(self, context):
    
            qcd_laycol = None
    
            slot_name = internals.qcd_slots.get_name(self.slot)
    
                qcd_laycol = internals.layer_collections[slot_name]["ptr"]
    
            orig_active_object = context.view_layer.objects.active
            locked = get_locked_objs(context)
    
                # check if slot can be toggled off.
    
                if not qcd_laycol.exclude:
    
                    if not set(locked.objs).isdisjoint(qcd_laycol.collection.objects):
                        return {'CANCELLED'}
    
    
                # toggle exclusion of qcd_laycol
    
                set_exclude_state(qcd_laycol, not qcd_laycol.exclude)
    
                # exclude all collections
    
                for laycol in internals.layer_collections.values():
    
                    if laycol["name"] != qcd_laycol.name:
    
                        # prevent exclusion if locked objects in this collection
    
                        if set(locked.objs).isdisjoint(laycol["ptr"].collection.objects):
    
                            laycol["ptr"].exclude = True
                        else:
                            laycol["ptr"].exclude = False
    
                # un-exclude target collection
    
                qcd_laycol.exclude = False
    
                # exclude all children
    
                def exclude_all_children(layer_collection):
    
                    # prevent exclusion if locked objects in this collection
    
                    if set(locked.objs).isdisjoint(layer_collection.collection.objects):
    
                        layer_collection.exclude = True
                    else:
                        layer_collection.exclude = False
    
    
                apply_to_children(qcd_laycol, exclude_all_children)
    
            if orig_active_object:
                if orig_active_object.name in context.view_layer.objects:
                    context.view_layer.objects.active = orig_active_object
    
    
            # restore locked objects back to their original mode
            # needed because of exclude child updates
    
            if context.view_layer.objects.active:
                if locked.objs:
                    bpy.ops.object.mode_set(mode=locked.mode)
    
            # set layer as active layer collection
            context.view_layer.active_layer_collection = qcd_laycol
    
            # update header UI
    
    
            view_layer = context.view_layer.name
    
            if view_layer in internals.rto_history["exclude"]:
                del internals.rto_history["exclude"][view_layer]
            if view_layer in internals.rto_history["exclude_all"]:
                del internals.rto_history["exclude_all"][view_layer]
    
    class UnassignedQCDSlot(Operator):
        bl_label = ""
        bl_idname = "view3d.unassigned_qcd_slot"
        bl_options = {'REGISTER', 'UNDO'}
    
        slot: StringProperty()
    
        @classmethod
        def description(cls, context, properties):
            slot_string = f"Unassigned QCD Slot {properties.slot}:\n"
    
            hotkey_string = (
                "  * LMB - Create slot.\n"
                "  * Shift+LMB - Create and isolate slot.\n"
                "  * Ctrl+LMB - Create and move objects to slot.\n"
                "  * Ctrl+Shift+LMB - Create and add objects to slot"
                )
    
            return f"{slot_string}{hotkey_string}"
    
        def invoke(self, context, event):
            modifiers = get_modifiers(event)
    
            new_collection = bpy.data.collections.new(f"Collection {self.slot}")
            context.scene.collection.children.link(new_collection)
            internals.qcd_slots.add_slot(f"{self.slot}", new_collection.name)
    
            # update tree view property
            update_property_group(context)
    
            if modifiers == {"shift"}:
                bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False)
    
            elif modifiers == {"ctrl"}:
                bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=False)
    
            elif modifiers == {"ctrl", "shift"}:
                bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=True)
    
            else:
                pass
    
            return {'FINISHED'}
    
    
    class CreateAllQCDSlots(Operator):
        bl_label = "Create All QCD Slots"
        bl_description = "Create any missing QCD slots so you have a full 20"
        bl_idname = "view3d.create_all_qcd_slots"
        bl_options = {'REGISTER', 'UNDO'}
    
        def execute(self, context):
            for slot_number in range(1, 21):
                if not internals.qcd_slots.get_name(f"{slot_number}"):
                    new_collection = bpy.data.collections.new(f"Collection {slot_number}")
                    context.scene.collection.children.link(new_collection)
                    internals.qcd_slots.add_slot(f"{slot_number}", new_collection.name)
    
            # update tree view property
            update_property_group(context)
    
            return {'FINISHED'}
    
    
    
    class RenumerateQCDSlots(Operator):
    
        bl_label = "Renumber QCD Slots"
    
        bl_description = (
    
            "Renumber QCD slots.\n"
    
            "  * LMB - Renumber (breadth first) from slot 1.\n"
            "  * +Ctrl - Linear.\n"
            "  * +Alt - Reset.\n"
            "  * +Shift - Constrain to branch"
    
        bl_idname = "view3d.renumerate_qcd_slots"
        bl_options = {'REGISTER', 'UNDO'}
    
        def invoke(self, context, event):
    
            modifiers = get_modifiers(event)
    
    
            beginning = False
            depth_first = False
            constrain = False
    
            if 'alt' in modifiers:
                beginning=True
    
            if 'ctrl' in modifiers:
                depth_first=True
    
            if 'shift' in modifiers:
                constrain=True
    
    
            internals.qcd_slots.renumerate(beginning=beginning,
    
                                 depth_first=depth_first,
                                 constrain=constrain)
    
            update_property_group(context)