OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / utils / mod_controller.py

717 lines · python ref:
import bpy
from mathutils import Vector
from . modifiers import get_mod_copy, transfer_mod_data
from .. utility import modifier
from .profile import get_bevel_profile, set_bevel_profile


class Mod_Data:
    def __init__(self, mod, created=False):
        self.mod = mod
        self.mod_copy = get_mod_copy(mod)
        self.was_created = created
        self.logically_deleted = False
        # Sort
        self.name = ''
        self.remove = False
        # Profiles
        self.profile = None

        self.__setup()


    def __setup(self):
        if not self.mod: return

        if self.mod.type == 'BEVEL':
            if hasattr(self.mod, 'use_custom_profile'):
                if self.mod.use_custom_profile:
                    self.profile = get_bevel_profile(self.mod)

            elif hasattr(self.mod, 'profile_type'):
                if self.mod.profile_type == 'CUSTOM':
                    self.profile = get_bevel_profile(self.mod)


    def revert(self):
        if not self.mod: return
        transfer_mod_data(self.mod, self.mod_copy)
        if self.mod.type == 'BEVEL':
            if self.profile:
                set_bevel_profile(self.mod, self.profile, False)


    def is_valid(self):
        if not self.mod: return False
        if self.logically_deleted: return False
        return True


class Obj_Data:
    def __init__(self, context, obj):
        self.obj = obj
        self.mod_datas = []
        self.index = 0
        self.mesh = None
        self.stroke = None
        self.dims = (0,0,0)
        self.scale = Vector((1,1,1))

        if type(obj) == bpy.types.Object:

            self.scale = obj.scale.copy()

            if type(obj.data) == bpy.types.Mesh:
                self.mesh = obj.data
                self.dims = get_obj_dims(context, obj)

            elif type(obj.data) == bpy.types.GreasePencil:
                self.stroke = obj.data
                self.dims = get_obj_dims(context, obj)


    def active_mod(self):
        '''The active mod being used'''

        if self.__ensure_index():
            return self.mod_datas[self.index].mod
        return None


    def active_mod_data(self):
        '''The active Mod Data being used'''

        if self.__ensure_index():
            return self.mod_datas[self.index]
        return None


    def all_active_mods(self):
        '''All mods that are valid / not loggicaly deleted'''

        mods = []
        for mod_data in self.mod_datas:
            if mod_data.logically_deleted == False:
                if mod_data.mod:
                    mods.append(mod_data.mod)
        return mods


    def __ensure_index(self):
        if self.index > len(self.mod_datas) - 1:
            self.index = len(self.mod_datas) - 1
        if self.index < 0:
            self.index = 0
        if len(self.mod_datas) == 0:
            return False
        return True


class Mod_Controller:
    def __init__(self, context, objs, type_map={}, create_new=False, active_obj=None):
        '''
        Type Map = KEY -> Object Type : VAL -> Mod Type
        Example = {bpy.types.Mesh : 'Array', bpy.types.GreasePencil : 'GP_ARRAY'}
        '''

        self.obj_datas = []
        self.type_map = type_map
        self.active_obj = active_obj

        # Setup
        for obj in objs:

            # Validate
            if not valid_object_type(self.type_map, obj): continue

            obj_data = Obj_Data(context, obj)

            # Mod Object
            mod_object = modifier_container(self.type_map, obj)
            if mod_object == None: continue

            # Mod Type
            valid_mod_type = mod_type_from_map(self.type_map, obj)
            if not valid_mod_type: continue

            # Capture
            for mod in mod_object:
                if mod.type == valid_mod_type:
                    mod_data = Mod_Data(mod)
                    mod.show_expanded = False

                    obj_data.mod_datas.append(mod_data)

            # Create
            if create_new or len(obj_data.mod_datas) == 0:
                mod = mod_object.new(valid_mod_type.title(), valid_mod_type)
                mod_data = Mod_Data(mod, created=True)
                mod.show_expanded = False

                obj_data.mod_datas.append(mod_data)

            # Store
            self.obj_datas.append(obj_data)

        # Index
        for obj_data in self.obj_datas:
            obj_data.index = len(obj_data.mod_datas) - 1
            if obj_data.index < 0:
                obj_data.index = 0

        # Active Validate
        valid = True
        if not self.active_obj: valid = False
        if type(self.active_obj) != bpy.types.Object: valid = False
        if type(self.active_obj.data) not in self.type_map: valid = False

        if valid == False:
            for obj_data in self.obj_datas:
                self.active_obj = obj_data.obj
                break
        else:
            # Search for active
            valid_active_obj = False
            for obj_data in self.obj_datas:
                if obj_data.obj == self.active_obj:
                    valid_active_obj = True
                    break
            # Fall back to first
            if not valid_active_obj:
                for obj_data in self.obj_datas:
                    self.active_obj = obj_data.obj
                    break


    def set_attr(self, attr='', value=0):
        '''Set attributes on all mods : contains saftey checks'''

        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            mod = obj_data.mod_datas[obj_data.index].mod
            if not mod: continue
            if not hasattr(mod, attr): continue
            setattr(mod, attr, value)

    # --- ACTIVE --- #

    def active_modifiers(self, with_obj=False):
        '''For each Obj Data -> return the active Modifier'''

        mods = []
        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            mod = obj_data.mod_datas[obj_data.index].mod
            if not mod: continue
            if with_obj: mods.append((obj_data.obj, mod))
            else: mods.append(mod)
        return mods


    def active_mod_datas(self):
        '''For each Obj Data -> return the active Mod Data'''

        datas = []
        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            datas.append(obj_data.mod_datas[obj_data.index])
        return datas


    def active_object_mod(self, as_obj_data=False):
        '''Active obj -> Return the active mod'''

        obj_data = self.__active_object_data()

        if obj_data:
            if self.__validate_index(obj_data):
                if as_obj_data: return obj_data
                return obj_data.mod_datas[obj_data.index].mod

        # Return first on failure
        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            if as_obj_data: return obj_data
            return obj_data.mod_datas[obj_data.index].mod

        return None


    def active_obj_mod_index(self):
        '''Active obj -> active mod data index'''

        obj_data = self.__active_object_data()
        if obj_data: return obj_data.index
        # Return first index
        for obj_data in self.obj_datas:
            return obj_data.index
        # Fallback
        return 0


    def active_obj_mods(self):
        '''All mods on active object / excludes logically deleted mods / invalid mods'''

        obj_data = self.__active_object_data()
        mods = []
        if not obj_data: return mods

        for mod_data in obj_data.mod_datas:
            if not mod_data.logically_deleted:
                if mod_data.mod:
                    mods.append(mod_data.mod)
        return mods

    # --- VALIDATED --- #

    def validated_obj_datas(self):
        datas = []
        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            datas.append(obj_data)
        return datas

    # --- CREATE / DELETE --- #

    def loggically_deleted_mods(self, active_only=False):
        '''Return all the logically deleted mods (Viewport display is off / flagged for delete)'''

        mods = []
        if active_only:
            for obj_data in self.obj_datas:
                if obj_data.obj != self.active_obj: continue
                for mod_data in obj_data.mod_datas:
                    if mod_data.logically_deleted:
                        mods.append(mod_data.mod)
                return mods

        for obj_data in self.obj_datas:
            for mod_data in obj_data.mod_datas:
                if mod_data.logically_deleted:
                    mods.append(mod_data.mod)
        return mods


    def remove_active_mod(self, leave_one=True, use_logical_delete=False, remove_if_created=False):
        '''Remove mods / Handle index safety'''

        def perma_remove_mod(obj_data):
            mod_data = obj_data.mod_datas[obj_data.index]
            mod = mod_data.mod
            mod_object = modifier_container(self.type_map, obj_data.obj)
            mod_object.remove(mod)
            obj_data.mod_datas.remove(mod_data)
            self.__set_next_available_index(obj_data)


        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue

            # Check if available mods is greater than 1
            if leave_one:
                count = 0
                for mod_data in obj_data.mod_datas:
                    if mod_data.logically_deleted: continue
                    count += 1
                if count <= 1: continue

            # Stop loggical delete if remove_if_created
            if remove_if_created:
                if obj_data.mod_datas[obj_data.index].was_created:
                    perma_remove_mod(obj_data)
                    continue


            if use_logical_delete:
                obj_data.mod_datas[obj_data.index].logically_deleted = True
                obj_data.mod_datas[obj_data.index].mod.show_viewport = False
                self.__set_next_available_index(obj_data)
                continue

            perma_remove_mod(obj_data)


    def create_new_mod(self, count_limit=None):
        '''Create a new mod on each object : uses type map for each object'''

        mods = []
        for obj_data in self.obj_datas:

            if type(count_limit) == int:
                count = 0
                for mod_data in obj_data.mod_datas:
                    if mod_data.logically_deleted == False:
                        count += 1
                if count > count_limit: continue

            obj = obj_data.obj

            mod_object = modifier_container(self.type_map, obj)
            if not mod_object: continue

            valid_mod_type = mod_type_from_map(self.type_map, obj)
            if not valid_mod_type: continue

            mod = mod_object.new(valid_mod_type.title(), valid_mod_type)
            mod_data = Mod_Data(mod, created=True)
            mod.show_expanded = False

            obj_data.mod_datas.append(mod_data)
            obj_data.index = len(obj_data.mod_datas) - 1
            mods.append(mod)
        return mods

    # --- MOVE : INDEX / STACK POSITION --- #

    def move_mod(self, context, up=True):
        '''Move active modifiers on the stack'''

        active_obj = context.active_object
        for obj, mod in self.active_modifiers(with_obj=True):
            context.view_layer.objects.active = obj
            if up: bpy.ops.object.modifier_move_up(modifier=mod.name)
            else: bpy.ops.object.modifier_move_down(modifier=mod.name)

        context.view_layer.objects.active = active_obj


    def clamped_next_mod_index(self, forward=True):
        '''Move the active mod index: stopping at clamped lengths'''

        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue

            og_index = obj_data.index
            if forward:
                # Already maxed
                if og_index == len(obj_data.mod_datas) - 1:
                    continue
                # Try next
                for index, mod_data in enumerate(obj_data.mod_datas):
                    if index > og_index:
                        if mod_data.logically_deleted == False:
                            obj_data.index = index
                            break
            else:
                # Check for first
                if og_index == 0: continue
                # Check previous
                count = og_index
                while count:
                    count -= 1
                    # Limit
                    if count < 0:
                        obj_data.index = og_index
                        break
                    # Search back for previous available
                    if obj_data.mod_datas[count].logically_deleted == False:
                        obj_data.index = count
                        break


    def cyclic_directional_mod_index(self, forward=True):
        '''Set the next mod index but using direction.'''

        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            og_index = obj_data.index
            valid_indexes = [i for i, mod_data in enumerate(obj_data.mod_datas) if mod_data.is_valid()]
            if not valid_indexes: continue

            new_index = None

            if forward:
                for index in valid_indexes:
                    if index > og_index:
                        new_index = index
                        break
                if new_index == None:
                    new_index = valid_indexes[0]

            else:
                for index in reversed(valid_indexes):
                    if index < og_index:
                        new_index = index
                        break
                if new_index == None:
                    new_index = valid_indexes[-1]

            if new_index == None:
                new_index = valid_indexes[-1]

            obj_data.index = new_index


    def cyclic_next_mod_index(self):
        '''Next mod index but cycling back around (Only attempts to go forward)'''

        for obj_data in self.obj_datas:
            if not self.__validate_index(obj_data): continue
            og_index = obj_data.index
            # Try to set next available mod
            set_to_first = True
            for index, mod_data in enumerate(obj_data.mod_datas):
                if index > og_index:
                    if obj_data.mod_datas[index].logically_deleted == False:
                        obj_data.index = index
                        set_to_first = False
                        break
            # Wasnt set : so use first available
            if set_to_first:
                for index, mod_data in enumerate(obj_data.mod_datas):
                    if obj_data.mod_datas[index].logically_deleted == False:
                        obj_data.index = index
                        break


    def set_active_obj_mod_index(self, mod_name=""):
        '''Set the index for the active object Mod Data by mod name -> Returns set status'''

        obj_data = self.__active_object_data()
        if not obj_data: return False

        for index, mod_data in enumerate(obj_data.mod_datas):
            mod = mod_data.mod
            if not mod: continue
            if mod.name == mod_name:
                obj_data.index = index
                return True

        return False

    # --- SORTING --- #

    def sort_mods(self, sort_types=modifier.sort_types):
        '''Sort modifiers of specified types'''

        for obj_data in self.obj_datas:
            obj = obj_data.obj

            if type(obj.data) != bpy.types.Mesh: continue

            # Orignal active mod name
            active_mod_name = None
            if self.__validate_index(obj_data):
                active_mod_name = obj_data.active_mod().name

            # Save name
            for mod_data in obj_data.mod_datas:
                if mod_data.mod:
                    mod_data.name = mod_data.mod.name
                    mod_data.mod = None

            # Sort
            modifier.sort(obj, types=sort_types, last=True)

            # Restore mod refs
            for mod_data in obj_data.mod_datas:
                if mod_data.name in obj.modifiers:
                    mod_data.mod = obj.modifiers[mod_data.name]
                else:
                    mod_data.remove = True

            # Ensure
            valid_mod_datas = [md for md in obj_data.mod_datas if not md.remove]
            obj_data.mod_datas = valid_mod_datas

            # Restore index
            for index, mod_data in enumerate(obj_data.mod_datas):
                if mod_data.mod:
                    if mod_data.mod.name == active_mod_name:
                        obj_data.index = index
                        break

    # --- MISC --- #

    def all_objs(self):
        '''All stored objects in Obj Datas list'''

        return [obj_data.obj for obj_data in self.obj_datas if obj_data.obj]


    def all_created_mods(self, with_objs=False):
        '''Every newly created modifier / does not take in to account loggically deleted mods'''

        if with_objs:
            data = {}
            for obj_data in self.obj_datas:
                for mod_data in obj_data.mod_datas:
                    if mod_data.was_created:
                        if obj_data.obj not in data:
                            data[obj_data.obj] = []
                        data[obj_data.obj].append(mod_data.mod)
            return data

        mods = []
        for obj_data in self.obj_datas:
            for mod_data in obj_data.mod_datas:
                if mod_data.was_created:
                    mods.append(mod_data.mod)
        return mods

    # --- EXITS --- #

    def cancel_exit(self):
        for obj_data in self.obj_datas:

            for mod_data in obj_data.mod_datas:
                mod_object = modifier_container(self.type_map, obj_data.obj)
                # Remove created mods
                if mod_data.was_created:
                    mod_object.remove(mod_data.mod)
                    mod_data.mod = None
                    continue
                # Remove logically deleted mods
                if mod_data.logically_deleted:
                    mod_object.remove(mod_data.mod)
                    continue
                # Revert original mods
                mod_data.revert()


    def confirm_exit(self):
        for obj_data in self.obj_datas:
            mod_object = modifier_container(self.type_map, obj_data.obj)
            for mod_data in obj_data.mod_datas:
                # Remove logically deleted mods
                if mod_data.logically_deleted:
                    if mod_data.mod:
                        mod_object.remove(mod_data.mod)
                        continue

    # --- PRIVATE --- #

    def __set_next_available_index(self, obj_data):
        '''Try to set the next index to next forward else start going backwards'''

        # Try next index
        for index, mod_data in enumerate(obj_data.mod_datas):
            if index > obj_data.index:
                if mod_data.logically_deleted == False:
                    obj_data.index = index
                    return
            else: break

        # Try previous index
        count = obj_data.index - 1

        if count < 0:
            obj_data.index = 0
            return

        while count:
            if obj_data.mod_datas[count].logically_deleted == False:
                obj_data.index = count
                return
            count -= 1
            if count < 0: break

        # Fallback
        obj_data.index = 0


    def __validate_index(self, obj_data):
        '''Attempt to ensure the Obj Data mod index -> return True if okay for use'''

        # Incorrect indexing values
        if obj_data.index > len(obj_data.mod_datas) - 1:
            obj_data.index = len(obj_data.mod_datas) - 1
        if obj_data.index < 0:
            obj_data.index = 0

        # No mod datas
        if len(obj_data.mod_datas) == 0: return False

        # Current mod is not in use : go through list and set last active
        if obj_data.mod_datas[obj_data.index].logically_deleted:
            valid = False
            for index, mod_data in enumerate(obj_data.mod_datas):
                if not mod_data.logically_deleted:
                    obj_data.index = index
                    valid = True
            if not valid: return False

        return True


    def __active_object_data(self):
        '''The active object's Obj Data'''

        if self.active_obj:
            for obj_data in self.obj_datas:
                if obj_data.obj == self.active_obj:
                    return obj_data
        return None

# --- Utils --- #

def valid_object_type(type_map, obj):
    '''Ensure a key : val mapping is present'''

    if type(obj) != bpy.types.Object: return False
    if not hasattr(obj, 'data'): return False

    if type(obj.data) in type_map: return True
    return False


def modifier_container(type_map, obj):
    '''The blender modifier struct (object.modifiers)'''

    if not valid_object_type(type_map, obj): return None

    gp_types = set()

    if hasattr(bpy.types, 'GreasePencil'):
        gp_types.add(bpy.types.GreasePencil)

    if hasattr(bpy.types, 'GreasePencilv3'):
        gp_types.add(bpy.types.GreasePencilv3)

    for bpy_type in type_map:
        if type(obj.data) == bpy_type:
            if bpy_type in gp_types:
                mods = getattr(obj, 'grease_pencil_modifiers', obj.modifiers)
                return mods
            if bpy_type == bpy.types.Mesh:
                return obj.modifiers
            if bpy_type == bpy.types.Curve:
                return obj.modifiers
    return None


def mod_type_from_map(type_map, obj):
    '''Map the object type to the proper modifier type'''

    if not valid_object_type(type_map, obj): return None
    return type_map[type(obj.data)]


def get_obj_dims(context, obj=None):
    '''Get the object dimensions without any array mods.'''

    original_states = {}

    for mod in obj.modifiers:
        if mod.type == 'ARRAY':
            original_states[mod] = mod.show_viewport
            mod.show_viewport = False

    context.view_layer.update()

    x_scale = abs(obj.scale[0]) if abs(obj.scale[0]) > 0 else 1
    y_scale = abs(obj.scale[1]) if abs(obj.scale[1]) > 0 else 1
    z_scale = abs(obj.scale[2]) if abs(obj.scale[2]) > 0 else 1

    dims = (
        obj.dimensions[0] / x_scale,
        obj.dimensions[1] / y_scale,
        obj.dimensions[2] / z_scale)

    for key, val in original_states.items():
        key.show_viewport = val

    return dims