OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / utility / modifier.py

579 lines · python ref:
import bpy

from mathutils import Vector
from ctypes import Structure, c_float, c_short, c_char, cast, POINTER
from . import ct_modifier, geometry_nodes

### DEVIATION FROM BC/KITOPS
from . import addon
def user_sort(obj):
    # ignore_weight = addon.preference().property.sort_bevel_ignore_weight
    # ignore_vgroup = addon.preference().property.sort_bevel_ignore_vgroup
    # ignore_verts = addon.preference().property.sort_bevel_ignore_only_verts
    # sort_depth = addon.preference().property.sort_depth
    # props = {'use_only_vertices': True} if bpy.app.version < (2, 90, 0) else {'affect': 'VERTICES'}
    # bevs = bevels(obj, weight=ignore_weight, vertex_group=ignore_vgroup, props=props if ignore_verts else {})
    # sort(obj, option=addon.preference().property, ignore=bevs, sort_depth=sort_depth, ignore_flag=addon.preference().property.sort_ignore_char, stop_flag=addon.preference().property.sort_stop_char)
    sort(obj, option=addon.preference().property)


def bevel_method(obj):
    for mod in obj.modifiers:
        if mod.type == "BEVEL":
            mod.limit_method = addon.preference().property.workflow_mode
### END DEVIATION

_graph_hash = {
    'Auto Smooth': {
        '82a873746244f4af9ad0a644b5a4fedc4ea296b183f313567d8c254c33f1096986036e7a2d89d8e1cbf159fe039afeb67c382fae2f18d1aa3791677c70be558e',
        'c39943bc2ed4864755a7b17042831d2356f150a0a499ea7be361941d717879fc8f1b5be73166b473592984624729fa854a96de353f5e36a6ffd2e9412b4d36f1'
    },
}

sort_types = [
    'NODES',
    'ARRAY',
    'MIRROR',
    'SOLIDIFY',
    'BEVEL',
    'WEIGHTED_NORMAL',
    'SIMPLE_DEFORM',
    'TRIANGULATE',
    'DECIMATE',
    'REMESH',
    'SUBSURF',
    'UV_PROJECT',
]

if bpy.app.version[:2] >= (2, 82):
    sort_types.insert(4, 'WELD')

sort_flag = '*'
ignore_flag = ' '
sort_last_flag = '!'
lock_above_flag = '.'
lock_below_flag = '^'
stop_flag = '_'


def sort(obj, option=None, types=[], first=False, last=False, typed_order=False):
    ignore = []
    modifiers = []
    modifiers_last = []
    modifiers_lock_above_below = []
    sortable = [mod for mod in obj.modifiers if not getattr(mod,'use_pin_to_last', False)]
    sort_depth = 0 if not option else option.sort_depth
    force_last = []

    if len(sortable) < 2 : return

    def modifier_insert(use_sort, use_last, modifier, is_sort_last_flag):
        if not use_sort and not is_sort_last_flag:
            return

        if is_sort_last_flag:
            for mod in modifiers_last[:]:
                if mod_type(modifier) == mod_type(mod):
                    if mod.name.startswith(sort_last_flag):
                        is_sort_last_flag = False
                        use_last = False
                        break

                    modifiers_last.remove(mod)

            for mod in modifiers:
                if is_sort_last_flag and mod_type(modifier) == mod_type(mod):
                    modifiers.remove(mod)

        if mod_type(modifier) not in [mod_type(mod) for mod in modifiers_last]:
            modifiers.insert(0, modifier)

        if use_last:
            modifiers_last.append(modifier)


    def modifier_check_above_below(use_sort, use_last, modifier, is_sort_last_flag, type='above'):
        _modifiers = obj.modifiers[:]
        index = _modifiers.index(mod)
        offset_index = index + 1 if type == 'above' else index -1

        if offset_index > len(_modifiers) - 1 or offset_index < 0:
            return

        modifiers_lock_above_below.append([type, _modifiers[offset_index], modifier])


    def modifier_check(modifier):
        if not modifier.name or modifier.name.startswith(sort_last_flag*2):
            return

        prop = F'sort_{mod_type(modifier).lower()}'
        use_sort = getattr(option, prop) if option and mod_type(modifier) in types else mod_type(modifier) in types or modifier.name.startswith(sort_flag)
        use_last = getattr(option, F'{prop}_last') if option and mod_type(modifier) in types else modifier.name.startswith(sort_last_flag)
        is_sort_last_flag = use_last and modifier.name.startswith(sort_last_flag)

        if first or last and not option:
            use_sort = mod_type(modifier) in types
            use_last = False
            is_sort_last_flag = False

        if modifier.name.startswith(lock_above_flag):
            modifier_check_above_below(use_sort, use_last, modifier, is_sort_last_flag, type='above')

        elif modifier.name.startswith(lock_below_flag):
            modifier_check_above_below(use_sort, use_last, modifier, is_sort_last_flag, type='below')

        else:
            modifier_insert(use_sort, use_last, modifier, is_sort_last_flag)

    if option and not option.sort_modifiers:
        return

    if not types:
        types = sort_types

    if sort_depth:
        length = len(sortable)

        if length > sort_depth:
            sortable = sortable[length - sort_depth - 1:]

    if option:
        ignore_weight = option.sort_bevel_ignore_weight
        ignore_vgroup = option.sort_bevel_ignore_vgroup
        ignore_verts = option.sort_bevel_ignore_only_verts
        props = {'use_only_vertices': True} if bpy.app.version[:2] < (2, 90) else {'affect': 'VERTICES'}
        for mod in bevels(obj, weight=ignore_weight, vertex_group=ignore_vgroup, props=props if ignore_verts else {}):
            ignore.append(mod)

    for mod in sortable[:]:
        if mod in ignore or mod.name.startswith(ignore_flag):
            sortable.remove(mod)
        if mod.name.startswith(sort_last_flag*2):
            force_last.append(mod)
            sortable.remove(mod)

    for index, mod in enumerate(sortable):
        if mod.name[0] == stop_flag:
            sortable = sortable[index+1:]

            break

    if typed_order:
        for type in types:
            for mod in reversed(sortable):
                if mod_type(mod) != type:
                    continue

                modifier_check(mod)
    else:
        for mod in reversed(sortable):
            modifier_check(mod)

    if not (modifiers or force_last) and not modifiers_lock_above_below:
        return

    for prop in modifiers_lock_above_below:
        if prop[1] not in modifiers:
            continue

        index = modifiers.index(prop[1])

        if prop[0] == 'above':
            modifiers.insert(index, prop[2])
            continue

        if index + 1 == len(modifiers):
            modifiers.append(prop[2])
            continue

        modifiers.insert(index + 1, prop[2])

    mod_move = ct_modifier.mod_move_first if first else ct_modifier.mod_move_last
    for mod in modifiers:
        mod_move(mod, ignore_pin=False)

    for mod in force_last:
        ct_modifier.mod_move_last(mod, ignore_pin=False)

    ct_modifier.update_object(obj, bpy.context)


def apply(obj, mod=None, visible=False, modifiers=[], ignore=[], types={}):
    apply = []
    keep = []

    if mod:
        apply.append(mod)

    else:
        for mod in obj.modifiers:
            if (not modifiers or mod in modifiers) and mod not in ignore and (not visible or mod.show_viewport) and (not types or mod.type in types):
                apply.append(mod)

    for mod in obj.modifiers:
        if mod not in apply:
            keep.append((mod, mod.show_viewport))

    if not apply:
        del keep

        return

    for mod in keep:
        mod[0].show_viewport = False

    shared_name = None
    if obj.data.users > 1:
        shared_name = obj.data.name
        obj.data = obj.data.copy()
    remesh_voxel_size = obj.data.remesh_voxel_size

    ob = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
    obj.data = bpy.data.meshes.new_from_object(ob)
    obj.data.remesh_voxel_size = remesh_voxel_size

    for mod in apply:
        obj.modifiers.remove(mod)

    for mod in keep:
        mod[0].show_viewport = mod[1]

    for o in bpy.context.view_layer.objects:
        if o.type != 'MESH':
            continue

        if o.data.name == shared_name:
            o.data = obj.data

    if shared_name:
        obj.data.name = shared_name

    del apply
    del keep


def bevels(obj, angle=False, weight=False, vertex_group=False, props={}):
    if not hasattr(obj, 'modifiers'):
        return []

    bevel_mods = [mod for mod in obj.modifiers if mod.type == 'BEVEL']

    if not angle and not weight and not vertex_group and not props:
        return bevel_mods

    modifiers = []

    if angle:
        for mod in bevel_mods:
            if mod.limit_method == 'ANGLE':
                modifiers.append(mod)

    if weight:
        for mod in bevel_mods:
            if mod.limit_method == 'WEIGHT':
                modifiers.append(mod)

    if vertex_group:
        for mod in bevel_mods:
            if mod.limit_method == 'VGROUP':
                modifiers.append(mod)

    if props:
        for mod in bevel_mods:
            if mod in modifiers:
                continue

            for pointer in props:
                prop = hasattr(mod, pointer) and getattr(mod, pointer) == props[pointer]
                if not prop:
                    continue

                modifiers.append(mod)

    return sorted(modifiers, key=lambda mod: bevel_mods.index(mod))


def unmodified_bounds(obj, exclude={}):
    disabled = []
    for mod in obj.modifiers:
        if exclude and mod.type not in exclude and mod.show_viewport:
            disabled.append(mod)
            mod.show_viewport = False

    if disabled:
        bpy.context.view_layer.update()

    bounds = [Vector(point[:]) for point in obj.bound_box[:]]

    for mod in disabled:
        mod.show_viewport = True

    del disabled

    return bounds


def stored(mod):
    exclude = {'__doc__', '__module__', '__slots__', '_RNA_UI', 'bl_rna', 'rna_type', 'face_count', 'is_override_data', 'particle_system', 'map_curve', 'execution_time', 'persistent_uid'}
    mod_copy = ModifierCopy()

    profile_point = lambda p: CurveProfilePoint(x=p.x, y=p.y, flag=p.flag, h1=p.h1, h2=p.h2, h1_loc=p.h1_loc, h2_loc=p.h2_loc)

    if mod.type == 'NODES':
        mod_copy.transfer_items = []

        for key, val in mod.items():

            if hasattr(val, '__len__') and type(val) != str:
                mod_copy.transfer_items.append((key, [v for v in val]))

            else:
                mod_copy.transfer_items.append((key, val))


    for pointer in dir(mod):
        if pointer not in exclude:

            type_string = str(type(getattr(mod, pointer))).split("'")[1]
            if mod.type == 'UV_PROJECT' and pointer =='projectors':
                setattr(mod_copy, pointer, [UvProjector(p) for p in mod.projectors])

            elif mod.type == 'BEVEL' and pointer == 'custom_profile':
                profile = type('custom_profile', (), {
                    'use_clip': mod.custom_profile.use_clip,
                    'use_sample_even_lengths': mod.custom_profile.use_sample_even_lengths,
                    'use_sample_straight_edges': mod.custom_profile.use_sample_straight_edges,
                    'points': [profile_point(cast(p.as_pointer(), POINTER(CurveProfilePoint)).contents) for p in mod.custom_profile.points]})

                setattr(mod_copy, pointer, profile)

            elif mod.type == 'HOOK' and pointer == 'matrix_inverse':
                setattr(mod_copy, pointer, getattr(mod, pointer).copy()) # XXX: use copy

            elif type_string not in {'bpy_prop_array', 'Vector'}:
                setattr(mod_copy, pointer, getattr(mod, pointer))

            else:
                setattr(mod_copy, pointer, list(getattr(mod, pointer)))

    return mod_copy



def new(obj, name=str(), _type='BEVEL', mod=None, props={}):
    new = None
    if mod:
        new = obj.modifiers.new(name=mod.name, type=mod.type)

        for pointer in dir(mod):
            if '__' in pointer or pointer in {'bl_rna', 'rna_type', 'type', 'face_count', 'falloff_curve', 'vertex_indices', 'vertex_indices_set', 'is_override_data', 'particle_system', 'map_curve', 'execution_time', 'persistent_uid', 'bakes', 'panels', 'transfer_items'}:
                continue

            elif mod.type == 'UV_PROJECT' and pointer =='projectors':
                new.projector_count = mod.projector_count
                for new_proj, old_proj in zip(new.projectors, mod.projectors):
                    new_proj.object = old_proj.object

            elif mod.type == 'BEVEL' and pointer == 'custom_profile':
                step = 1 / len(mod.custom_profile.points)
                for index, point in enumerate(mod.custom_profile.points[1:-1]):
                    new.custom_profile.points.add(index * step, (index + 1) * step)

                for index, point in enumerate(mod.custom_profile.points):
                    point = cast(point.as_pointer(), POINTER(CurveProfilePoint)).contents if hasattr(point,'as_pointer') else point
                    new_point = cast(new.custom_profile.points[index].as_pointer(), POINTER(CurveProfilePoint)).contents

                    new_point.x = point.x
                    new_point.y = point.y
                    new_point.flag = point.flag
                    new_point.h1 = point.h1
                    new_point.h2 = point.h2
                    new_point.h1_loc = point.h1_loc
                    new_point.h2_loc = point.h2_loc

                new.custom_profile.update()

                new.custom_profile.use_clip = mod.custom_profile.use_clip
                new.custom_profile.use_sample_even_lengths = mod.custom_profile.use_sample_even_lengths
                new.custom_profile.use_sample_straight_edges = mod.custom_profile.use_sample_straight_edges

            else:
                try:
                    setattr(new, pointer, getattr(mod, pointer))
                except:
                    continue

        if mod.type == 'HOOK':
            new.matrix_inverse = mod.matrix_inverse # XXX: needs to be set after new.object
            new.vertex_indices_set(mod.vertex_indices)

        elif mod.type == 'NODES':
            if hasattr(mod, 'transfer_items'):
                for key, val in mod.transfer_items:

                    if isinstance(new.get(key, None), bool):
                        val = bool(val)

                    new[key] = val
            else:
                for key, val in mod.trtransfer_itemsan:
                    if hasattr(val, '__len__') and type(val) != str:
                        new[key] = [v for v in val]

                    else:
                        if isinstance(new.get(key, None), bool):
                            val = bool(val)

                        new[key] = val

    elif _type:
        new = obj.modifiers.new(name=name, type=_type)

        if props:
            for pointer in props:
                if hasattr(new, pointer):
                    setattr(new, pointer, props[pointer])

    return new


def exists(obj, full_match=True, types={}, **props):
    if not obj.modifiers:
        return False

    item = props.items()

    if not item:
        return bool(obj.modifiers) if not types else bool(any(mod.type in types for mod in obj.modifiers))

    checked = []
    for key, arg in item:
        checked.append(any(hasattr(mod, key) and getattr(mod, key) == arg or mod.type in types) for mod in obj.modifiers)

    return all(checked) if full_match else any(checked)


def collect(obj, full_match=False, types={}, **props):
    if not obj.modifiers:
        return []

    item = props.items()

    if not item:
        return obj.modifiers[:] if not types else [mod for mod in obj.modifiers if mod.type in types]

    check = lambda m, i: ((hasattr(m, k) and getattr(m, k) == a) for k, a in i) or m.type in types
    validated = lambda m, i: all(check(m, i)) if full_match else any(check(m, i))

    modifiers = []
    for mod in obj.modifiers:
        if not validated(mod, item):
            continue

        modifiers.append(mod)

    return modifiers


def move_to_index(mod, index=0, rebuild=False):
    from . import operator_override

    count = len(mod.id_data.modifiers)

    if index < 0:
        index = count - (abs(index) % count)

    else:
        index = index % count

    if bpy.app.version[:2] >= (2, 90) and not rebuild:
        override = {'object' : mod.id_data, 'active_object' : mod.id_data}
        operator_override(bpy.context, bpy.ops.object.modifier_move_to_index, override, modifier=mod.name, index=index)

    else:
        obj = mod.id_data
        modifiers = [stored(m) for m in obj.modifiers if m != mod]
        modifiers.insert(index, stored(mod))

        obj.modifiers.clear()

        for mod in modifiers:
            new(obj, mod=mod)


def graph_hash(mod, limit=0):
    from . import hash_iter
    return hash_iter(mod.node_group.nodes, 'type', limit=limit)


def hashed_graph(mod):
    if mod.type != 'NODES':
        return ''

    for type in _graph_hash:
        if graph_hash(mod) in _graph_hash[type]:
            return type

    return ''

def mod_type(mod) -> str:
    '''Get Moidifer type or spoof it for known node groups'''
    if mod.type == 'NODES'and geometry_nodes.NodeBoolean.is_valid_modifier(mod): return 'BOOLEAN'

    return mod.type

def bool_object_get(mod):
    '''Get object from boolean or supported node group'''
    if mod.type == 'BOOLEAN': return mod.object

    wrapper = geometry_nodes.NodeBoolean.new(mod)
    if wrapper: return wrapper.object

    return None

def bool_operand_get(mod, default=None):
    '''Get operand from boolean or supported node group'''
    if mod.type == 'BOOLEAN': return getattr(mod, 'operand_type', default)

    wrapper = geometry_nodes.NodeBoolean.new(mod)
    if wrapper: return wrapper.operand_type

    return default

def bool_opeartion_get(mod, default=''):
    '''Get operation from boolean or supported node group'''
    if mod.type == 'BOOLEAN': mod.operation

    wrapper = geometry_nodes.NodeBoolean.new(mod)
    if wrapper: return wrapper.operation

    return default

def bool_mod_get(mod):
    '''Return NodeBoolean wrapper if mod is wrappable, passthrough if mod is BOOLEAN, otherwise None'''
    if mod.type == 'BOOLEAN':
        return mod

    wrapper = geometry_nodes.NodeBoolean.new(mod)
    return wrapper if wrapper else None

class CurveProfilePoint(Structure):
    _fields_ = [
        ('x', c_float),
        ('y', c_float),
        ('flag', c_short),
        ('h1', c_char),
        ('h2', c_char),
        ('h1_loc', c_float * 2),
        ('h2_loc', c_float * 2)]

class ModifierCopy():
    pass

class UvProjector():
    object:None

    def __init__(self, projector) -> None:
        self.object = projector.object