Start of the Major Overhaul
I decided to go through each function and UI section one by one, improving and overhauling things. Each function and section is going to be fully tested and not rushed out. This is the best way to catch things, but also include the code base as much as possible.
This commit is contained in:
+72
-413
@@ -1,268 +1,83 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from .dictionaries import bone_names
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
import typing
|
||||
from bpy.types import Context, Object
|
||||
from typing import Optional, Tuple, List, Set
|
||||
from ..core.translations import t
|
||||
from ..core.dictionaries import bone_names
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup
|
||||
from functools import lru_cache
|
||||
from bpy.props import PointerProperty, IntProperty, StringProperty
|
||||
from bpy.utils import register_class
|
||||
|
||||
|
||||
|
||||
|
||||
class SceneMatClass(PropertyGroup):
|
||||
mat: PointerProperty(type=Material)
|
||||
|
||||
register_class(SceneMatClass)
|
||||
|
||||
class MaterialListBool:
|
||||
#For the love that is holy do not ever touch these. If this was java I would make these private
|
||||
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
|
||||
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
|
||||
|
||||
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
|
||||
old_list: dict[str,list[Material]] = {}
|
||||
bool_material_list_expand: dict[str,bool] = {}
|
||||
|
||||
def set_bool(self, value: bool) -> None:
|
||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
|
||||
if value == False:
|
||||
MaterialListBool.old_list[bpy.context.scene.name] = []
|
||||
|
||||
def get_bool(self) -> bool:
|
||||
newlist: list[Material] = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if len(obj.material_slots)>0:
|
||||
for mat_slot in obj.material_slots:
|
||||
if mat_slot.material:
|
||||
if mat_slot.material not in newlist:
|
||||
newlist.append(mat_slot.material)
|
||||
|
||||
still_the_same: bool = True
|
||||
if bpy.context.scene.name in MaterialListBool.old_list:
|
||||
for item in newlist:
|
||||
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||
still_the_same = False
|
||||
break
|
||||
for item in MaterialListBool.old_list[bpy.context.scene.name]:
|
||||
if item not in newlist:
|
||||
still_the_same = False
|
||||
break
|
||||
else:
|
||||
still_the_same = False
|
||||
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
||||
|
||||
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
||||
|
||||
|
||||
### Clean up material names in the given mesh by removing the '.001' suffix.
|
||||
def clean_material_names(mesh: Mesh) -> None:
|
||||
for j, mat in enumerate(mesh.material_slots):
|
||||
if mat.name.endswith(('.0+', ' 0+')):
|
||||
mesh.active_material_index = j
|
||||
mesh.active_material.name = mat.name[:-len(mat.name.rstrip('0')) - 1]
|
||||
|
||||
# This will fix faulty uv coordinates, cats did this a other way which can have unintended consequences,
|
||||
# this is the best way i could of think of doing this for the time being, however may need improvements.
|
||||
|
||||
def fix_uv_coordinates(context: Context) -> None:
|
||||
obj = context.object
|
||||
|
||||
# Store current mode and selection
|
||||
current_mode = context.mode
|
||||
current_active = context.view_layer.objects.active
|
||||
current_selected = context.selected_objects.copy()
|
||||
|
||||
# Ensure we're in object mode and select the object
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
# Check if the object has any mesh data
|
||||
if obj.type == 'MESH' and obj.data:
|
||||
|
||||
# Switch to Edit Mode
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Select all UVs
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
|
||||
# Try to find UV Editor area, fall back to 3D View if not found
|
||||
area = next((area for area in context.screen.areas if area.type == 'UV_EDITOR'), None)
|
||||
if not area:
|
||||
area = next((area for area in context.screen.areas if area.type == 'VIEW_3D'), None)
|
||||
|
||||
# Get the region and space data
|
||||
region = next((region for region in area.regions if region.type == 'WINDOW'), None)
|
||||
space_data = area.spaces.active
|
||||
|
||||
# Create a context override
|
||||
override = {
|
||||
'area': area,
|
||||
'region': region,
|
||||
'space_data': space_data,
|
||||
'edit_object': obj,
|
||||
'active_object': obj,
|
||||
'selected_objects': [obj],
|
||||
'mode': 'EDIT_MESH',
|
||||
}
|
||||
|
||||
try:
|
||||
# Ensure UVs are selected
|
||||
bpy.ops.uv.select_all(override, action='SELECT')
|
||||
# Average UV island scales
|
||||
bpy.ops.uv.average_islands_scale(override)
|
||||
except Exception as e:
|
||||
print(f"UV Fix - Error during UV scaling: {str(e)}")
|
||||
|
||||
# Switch back to Object Mode
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
print("UV Fix - Switched back to Object Mode")
|
||||
|
||||
# Restore previous selection and active object
|
||||
for sel_obj in current_selected:
|
||||
sel_obj.select_set(True)
|
||||
context.view_layer.objects.active = current_active
|
||||
else:
|
||||
print("UV Fix - Object is not a valid mesh with UV data")
|
||||
|
||||
def has_shapekeys(mesh_obj: Object) -> bool:
|
||||
return mesh_obj.data.shape_keys is not None
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray:
|
||||
return np.array([v.co for v in shape_key.data])
|
||||
|
||||
def simplify_bonename(n: str) -> str:
|
||||
return n.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
||||
|
||||
def get_armature(context: Context, armature_name: Optional[str] = None) -> Optional[Object]:
|
||||
if armature_name:
|
||||
obj = bpy.data.objects[armature_name]
|
||||
if obj.type == "ARMATURE":
|
||||
return obj
|
||||
else:
|
||||
return None
|
||||
if context.view_layer.objects.active:
|
||||
obj = context.view_layer.objects.active
|
||||
if obj.type == "ARMATURE":
|
||||
return obj
|
||||
return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None)
|
||||
|
||||
def get_armatures(self, context: Context) -> List[Tuple[str, str, str]]:
|
||||
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
||||
if not armatures:
|
||||
return [('NONE', 'No Armature', '')]
|
||||
return armatures
|
||||
|
||||
def get_armatures_that_are_not_selected(self, context: Context) -> List[Tuple[str, str, str]]:
|
||||
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if ((obj.type == 'ARMATURE') and (obj.name != context.scene.avatar_toolkit.selected_armature))]
|
||||
if not armatures:
|
||||
return [('NONE', 'No Other Armature', '')]
|
||||
return armatures
|
||||
|
||||
def get_selected_armature(context: Context) -> Optional[Object]:
|
||||
try:
|
||||
if hasattr(context.scene, 'avatar_toolkit'):
|
||||
armature_name = context.scene.avatar_toolkit.selected_armature
|
||||
if isinstance(armature_name, bytes):
|
||||
try:
|
||||
armature_name = armature_name.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
armature_name = armature_name.decode('gbk') # For Chinese characters
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
armature_name = armature_name.decode('shift-jis')
|
||||
except UnicodeDecodeError:
|
||||
armature_name = armature_name.decode('latin1')
|
||||
|
||||
if armature_name:
|
||||
armature = bpy.data.objects.get(str(armature_name))
|
||||
if is_valid_armature(armature):
|
||||
return armature
|
||||
except Exception:
|
||||
pass
|
||||
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||
armature_name = context.scene.avatar_toolkit.active_armature
|
||||
if armature_name and armature_name != 'NONE':
|
||||
return bpy.data.objects.get(armature_name)
|
||||
return None
|
||||
|
||||
def get_merge_armature_source(context: Context) -> Optional[Object]:
|
||||
try:
|
||||
if hasattr(context.scene, 'merge_armature_source'):
|
||||
source_name = context.scene.merge_armature_source
|
||||
if isinstance(source_name, bytes):
|
||||
try:
|
||||
source_name = source_name.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
source_name = source_name.decode('shift-jis')
|
||||
except UnicodeDecodeError:
|
||||
source_name = source_name.decode('latin1', errors='ignore')
|
||||
def set_active_armature(context: bpy.types.Context, armature: bpy.types.Object) -> None:
|
||||
"""Set the active armature for Avatar Toolkit operations"""
|
||||
context.scene.avatar_toolkit.active_armature = armature
|
||||
|
||||
def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tuple[str, str, str]]:
|
||||
"""Get list of all armature objects in the scene"""
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
||||
if not armatures:
|
||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||
return armatures
|
||||
|
||||
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, str]:
|
||||
"""
|
||||
Validate if the selected object is a proper armature and has required bones
|
||||
Returns tuple of (is_valid, message)
|
||||
"""
|
||||
if not armature:
|
||||
return False, t("Armature.validation.no_armature")
|
||||
if armature.type != 'ARMATURE':
|
||||
return False, t("Armature.validation.not_armature")
|
||||
if not armature.data.bones:
|
||||
return False, t("Armature.validation.no_bones")
|
||||
|
||||
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
||||
found_bones: Set[str] = {bone.name.lower() for bone in armature.data.bones}
|
||||
|
||||
for bone in essential_bones:
|
||||
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
||||
return False, t("Armature.validation.missing_bone", bone=bone)
|
||||
|
||||
if source_name:
|
||||
return bpy.data.objects.get(str(source_name))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
return True, t("QuickAccess.valid_armature")
|
||||
|
||||
def set_selected_armature(context: Context, armature: Optional[Object]) -> None:
|
||||
context.scene.avatar_toolkit.selected_armature = armature.name if armature else ""
|
||||
def auto_select_single_armature(context: bpy.types.Context) -> None:
|
||||
"""Automatically select armature if only one exists in scene"""
|
||||
armatures = get_armature_list(context)
|
||||
if len(armatures) == 1:
|
||||
set_active_armature(context, armatures[0])
|
||||
|
||||
def is_valid_armature(armature: Object) -> bool:
|
||||
if not armature or armature.type != 'ARMATURE':
|
||||
return False
|
||||
if not armature.data or not armature.data.bones:
|
||||
return False
|
||||
return True
|
||||
def clear_default_objects() -> None:
|
||||
"""Removes default Blender objects (cube, light, camera)"""
|
||||
default_names: Set[str] = {'Cube', 'Light', 'Camera'}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name.split('.')[0] in default_names:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
def select_current_armature(context: Context) -> bool:
|
||||
armature = get_selected_armature(context)
|
||||
def get_armature_stats(armature: bpy.types.Object) -> dict:
|
||||
"""Get statistics about the armature"""
|
||||
return {
|
||||
'bone_count': len(armature.data.bones),
|
||||
'has_pose': bool(armature.pose),
|
||||
'visible': not armature.hide_viewport,
|
||||
'name': armature.name
|
||||
}
|
||||
|
||||
def get_all_meshes(context: Context) -> List[Object]:
|
||||
armature = get_active_armature(context)
|
||||
if armature:
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
armature.select_set(True)
|
||||
context.view_layer.objects.active = armature
|
||||
return True
|
||||
return False
|
||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||
return []
|
||||
|
||||
def apply_shapekey_to_basis(context: bpy.types.Context, obj: bpy.types.Object, shape_key_name: str, delete_old: bool = False) -> bool:
|
||||
if shape_key_name not in obj.data.shape_keys.key_blocks:
|
||||
return False
|
||||
shapekeynum = obj.data.shape_keys.key_blocks.find(shape_key_name)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
|
||||
|
||||
obj.active_shape_key_index = 0
|
||||
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=1)
|
||||
obj.active_shape_key_index = shapekeynum
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=-2)
|
||||
|
||||
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
print("blended!")
|
||||
|
||||
if delete_old:
|
||||
obj.active_shape_key_index = shapekeynum
|
||||
bpy.ops.object.shape_key_remove(all=False)
|
||||
else:
|
||||
mesh: bpy.types.Mesh = obj.data
|
||||
mesh.shape_keys.key_blocks[shape_key_name].name = shape_key_name + "_reversed"
|
||||
return True
|
||||
|
||||
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Object]) -> bool:
|
||||
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Object]) -> bool:
|
||||
for mesh_obj in meshes:
|
||||
if not mesh_obj.data:
|
||||
continue
|
||||
|
||||
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
|
||||
if len(mesh_obj.data.shape_keys.key_blocks) == 1:
|
||||
basis = mesh_obj.data.shape_keys.key_blocks[0]
|
||||
@@ -274,11 +89,10 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Obje
|
||||
apply_armature_to_mesh_with_shapekeys(armature_obj, mesh_obj, context)
|
||||
else:
|
||||
apply_armature_to_mesh(armature_obj, mesh_obj)
|
||||
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
bpy.ops.pose.armature_apply(selected=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return True
|
||||
|
||||
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
||||
@@ -290,7 +104,7 @@ def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
||||
else:
|
||||
for _ in range(len(mesh_obj.modifiers) - 1):
|
||||
bpy.ops.object.modifier_move_up(modifier=armature_mod.name)
|
||||
|
||||
|
||||
with bpy.context.temp_override(object=mesh_obj):
|
||||
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
|
||||
|
||||
@@ -298,10 +112,11 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
old_active_index = mesh_obj.active_shape_key_index
|
||||
old_show_only = mesh_obj.show_only_shape_key
|
||||
mesh_obj.show_only_shape_key = True
|
||||
|
||||
|
||||
shape_keys = mesh_obj.data.shape_keys.key_blocks
|
||||
vertex_groups = []
|
||||
mutes = []
|
||||
|
||||
for sk in shape_keys:
|
||||
vertex_groups.append(sk.vertex_group)
|
||||
sk.vertex_group = ''
|
||||
@@ -316,7 +131,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
arm_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
|
||||
arm_mod.object = armature_obj
|
||||
|
||||
|
||||
co_length = len(mesh_obj.data.vertices) * 3
|
||||
eval_cos = np.empty(co_length, dtype=np.single)
|
||||
|
||||
@@ -333,6 +148,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
for mod in disabled_mods:
|
||||
mod.show_viewport = True
|
||||
|
||||
mesh_obj.modifiers.remove(arm_mod)
|
||||
|
||||
for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
|
||||
@@ -341,160 +157,3 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
mesh_obj.active_shape_key_index = old_active_index
|
||||
mesh_obj.show_only_shape_key = old_show_only
|
||||
|
||||
def get_all_meshes(context: Context) -> List[Object]:
|
||||
armature = get_selected_armature(context)
|
||||
if armature and is_valid_armature(armature):
|
||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||
return []
|
||||
|
||||
def get_mesh_items(self, context):
|
||||
return [(obj.name, obj.name, "") for obj in get_all_meshes(context)]
|
||||
|
||||
def open_web_after_delay_multi_threaded(delay: typing.Optional[float] = 1.0, url: typing.Union[str, typing.Any] = ""):
|
||||
thread = threading.Thread(target=open_web_after_delay,args=[delay,url],name="open_browser_thread")
|
||||
thread.start()
|
||||
|
||||
def open_web_after_delay(delay, url):
|
||||
print("opening browser in "+str(delay)+" seconds.")
|
||||
time.sleep(delay)
|
||||
|
||||
webbrowser.open_new_tab(url)
|
||||
|
||||
def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
arm = bpy.context.object.data
|
||||
cb = arm.edit_bones.new(b.name)
|
||||
|
||||
cb.head = b.head
|
||||
cb.tail = b.tail
|
||||
cb.matrix = b.matrix
|
||||
cb.parent = b.parent
|
||||
return cb
|
||||
|
||||
def has_shapekeys(mesh_obj: Object) -> bool:
|
||||
return mesh_obj.data.shape_keys is not None
|
||||
|
||||
def sort_shape_keys(mesh: Object) -> None:
|
||||
print("Starting shape key sorting...")
|
||||
if not has_shapekeys(mesh):
|
||||
print("No shape keys found. Exiting sort function.")
|
||||
return
|
||||
|
||||
# Set the mesh as the active object
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
order = [
|
||||
'Basis',
|
||||
'vrc.blink_left',
|
||||
'vrc.blink_right',
|
||||
'vrc.lowerlid_left',
|
||||
'vrc.lowerlid_right',
|
||||
'vrc.v_aa',
|
||||
'vrc.v_ch',
|
||||
'vrc.v_dd',
|
||||
'vrc.v_e',
|
||||
'vrc.v_ff',
|
||||
'vrc.v_ih',
|
||||
'vrc.v_kk',
|
||||
'vrc.v_nn',
|
||||
'vrc.v_oh',
|
||||
'vrc.v_ou',
|
||||
'vrc.v_pp',
|
||||
'vrc.v_rr',
|
||||
'vrc.v_sil',
|
||||
'vrc.v_ss',
|
||||
'vrc.v_th',
|
||||
]
|
||||
|
||||
shape_keys = mesh.data.shape_keys.key_blocks
|
||||
print(f"Total shape keys: {len(shape_keys)}")
|
||||
|
||||
# Create a list of shape key names in their current order
|
||||
current_order = [key.name for key in shape_keys]
|
||||
|
||||
# Create a new order list
|
||||
new_order = []
|
||||
|
||||
# First, add all the keys that are in the predefined order
|
||||
for name in order:
|
||||
if name in current_order:
|
||||
new_order.append(name)
|
||||
current_order.remove(name)
|
||||
|
||||
# Then add any remaining keys that weren't in the predefined order
|
||||
new_order.extend(current_order)
|
||||
|
||||
print("New order:", new_order)
|
||||
|
||||
# Now, rearrange the shape keys based on the new order
|
||||
for i, name in enumerate(new_order):
|
||||
index = shape_keys.find(name)
|
||||
if index != i:
|
||||
print(f"Moving {name} from index {index} to {i}")
|
||||
mesh.active_shape_key_index = index
|
||||
while mesh.active_shape_key_index > i:
|
||||
bpy.ops.object.shape_key_move(type='UP')
|
||||
|
||||
print("Shape key sorting completed.")
|
||||
|
||||
def get_shapekeys(mesh: Object, prefix: str = '') -> List[tuple]:
|
||||
if not has_shapekeys(mesh):
|
||||
return []
|
||||
return [(key.name, key.name, key.name) for key in mesh.data.shape_keys.key_blocks if key.name != 'Basis' and key.name.startswith(prefix)]
|
||||
|
||||
def remove_default_objects():
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name in ["Camera", "Light", "Cube"]:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
def init_progress(context, steps):
|
||||
context.window_manager.progress_begin(0, 100)
|
||||
context.scene.avatar_toolkit.progress_steps = steps
|
||||
context.scene.avatar_toolkit.progress_current = 0
|
||||
|
||||
def update_progress(self, context, message):
|
||||
context.scene.avatar_toolkit.progress_current += 1
|
||||
progress = (context.scene.avatar_toolkit.progress_current / context.scene.avatar_toolkit.progress_steps) * 100
|
||||
context.window_manager.progress_update(progress)
|
||||
context.area.header_text_set(message)
|
||||
self.report({'INFO'}, message)
|
||||
|
||||
def finish_progress(context):
|
||||
context.window_manager.progress_end()
|
||||
context.area.header_text_set(None)
|
||||
|
||||
def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_group: str, target_group: str, delete_source_group: bool = True) -> bool:
|
||||
# Create and configure the Vertex Weight Mix modifier
|
||||
modifier = obj.modifiers.new(name="merge_weights", type="VERTEX_WEIGHT_MIX")
|
||||
modifier.show_viewport = True
|
||||
modifier.show_render = True
|
||||
modifier.mix_set = 'B'
|
||||
modifier.vertex_group_a = target_group
|
||||
modifier.vertex_group_b = source_group
|
||||
modifier.mask_constant = 1.0
|
||||
|
||||
# Ensure we're in Object Mode
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Deselect all objects and select only our target object
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
# Move modifier to the top of the stack
|
||||
if len(obj.modifiers) > 1:
|
||||
obj.modifiers.move(obj.modifiers.find(modifier.name), 0)
|
||||
|
||||
# Apply modifier with correct syntax
|
||||
with context.temp_override(active_object=obj):
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
|
||||
# Clean up
|
||||
if delete_source_group and source_group in obj.vertex_groups:
|
||||
obj.vertex_groups.remove(obj.vertex_groups[source_group])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user