2025-12-01
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from . import (
|
||||
delete_empty_deform_groups,
|
||||
delete_unselected_deform_groups,
|
||||
delete_unused_groups,
|
||||
focus_deforming_bones,
|
||||
symmetrize_weights,
|
||||
)
|
||||
|
||||
modules = [
|
||||
delete_empty_deform_groups,
|
||||
delete_unselected_deform_groups,
|
||||
delete_unused_groups,
|
||||
focus_deforming_bones,
|
||||
symmetrize_weights,
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Operator, Object, VertexGroup
|
||||
from bpy.utils import flip_name
|
||||
|
||||
from ..utils import delete_vgroups, poll_deformed_mesh_with_vgroups, get_deforming_vgroups
|
||||
|
||||
|
||||
class EASYWEIGHT_OT_delete_empty_deform_groups(Operator):
|
||||
"""Delete vertex groups which are associated to deforming bones but don't have any weights"""
|
||||
|
||||
bl_idname = "object.delete_empty_deform_vgroups"
|
||||
bl_label = "Delete Empty Deform Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_deformed_mesh_with_vgroups(cls, context)
|
||||
|
||||
def execute(self, context):
|
||||
empty_groups = get_empty_deforming_vgroups(context.active_object)
|
||||
|
||||
num_groups = len(empty_groups)
|
||||
print(f"Deleting empty deform groups:")
|
||||
print(" " + "\n ".join([vg.name for vg in empty_groups]))
|
||||
|
||||
delete_vgroups(context.active_object, empty_groups)
|
||||
|
||||
self.report({'INFO'}, f"Deleted {num_groups} empty deform groups.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_non_deforming_vgroups(mesh_ob: Object) -> set:
|
||||
"""Get the vertex groups of a mesh object that don't correspond to a deform bone in the given armature."""
|
||||
deforming_vgroups = get_deforming_vgroups(mesh_ob)
|
||||
return set(mesh_ob.vertex_groups) - set(deforming_vgroups)
|
||||
|
||||
|
||||
def get_empty_deforming_vgroups(mesh_ob: Object) -> list[VertexGroup]:
|
||||
deforming_vgroups = get_deforming_vgroups(mesh_ob)
|
||||
empty_deforming_groups = [vg for vg in deforming_vgroups if not vgroup_has_weight(mesh_ob, vg)]
|
||||
|
||||
# If there's no Mirror modifier, we're done.
|
||||
if not 'MIRROR' in (m.type for m in mesh_ob.modifiers):
|
||||
return empty_deforming_groups
|
||||
|
||||
# Mirror Modifier: A group is not considered empty if it is the opposite
|
||||
# of a non-empty group.
|
||||
for vgroup in empty_deforming_groups[:]:
|
||||
opp_vgroup = mesh_ob.vertex_groups.get(flip_name(vgroup.name))
|
||||
if not opp_vgroup:
|
||||
continue
|
||||
if opp_vgroup not in empty_deforming_groups:
|
||||
empty_deforming_groups.remove(vgroup)
|
||||
|
||||
return empty_deforming_groups
|
||||
|
||||
|
||||
def get_vgroup_weight_on_vert(vgroup, vert_idx) -> float:
|
||||
# Despite how terrible this is, as of 04/Jun/2021 it seems to be the
|
||||
# only only way to ask Blender if a vertex is assigned to a vertex group.
|
||||
try:
|
||||
weight = vgroup.weight(vert_idx)
|
||||
return weight
|
||||
except RuntimeError:
|
||||
return -1
|
||||
|
||||
|
||||
def vgroup_has_weight(mesh_ob, vgroup) -> bool:
|
||||
for i in range(0, len(mesh_ob.data.vertices)):
|
||||
if get_vgroup_weight_on_vert(vgroup, i) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_OT_delete_empty_deform_groups]
|
||||
@@ -0,0 +1,38 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..utils import delete_vgroups, poll_weight_paint_mode, get_deforming_vgroups
|
||||
|
||||
|
||||
class EASYWEIGHT_OT_delete_unselected_deform_groups(Operator):
|
||||
"""Delete deforming vertex groups that do not correspond to any selected pose bone"""
|
||||
|
||||
bl_idname = "object.delete_unselected_deform_vgroups"
|
||||
bl_label = "Delete Unselected Deform Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_weight_paint_mode(cls, context, with_rig=True, with_groups=True)
|
||||
|
||||
def execute(self, context):
|
||||
deforming_groups = get_deforming_vgroups(context.active_object)
|
||||
selected_bone_names = [b.name for b in context.selected_pose_bones]
|
||||
unselected_def_groups = [
|
||||
vg for vg in deforming_groups if vg.name not in selected_bone_names
|
||||
]
|
||||
|
||||
print(f"Deleting unselected deform groups:")
|
||||
deleted_names = [vg.name for vg in unselected_def_groups]
|
||||
print(" " + "\n ".join(deleted_names))
|
||||
|
||||
delete_vgroups(context.active_object, unselected_def_groups)
|
||||
|
||||
self.report({'INFO'}, f"Deleted {len(deleted_names)} unselected deform groups.")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_OT_delete_unselected_deform_groups]
|
||||
@@ -0,0 +1,160 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator, Object, VertexGroup, Modifier
|
||||
|
||||
from ..utils import delete_vgroups, poll_deformed_mesh_with_vgroups, get_deforming_vgroups
|
||||
|
||||
|
||||
class EASYWEIGHT_OT_delete_unused_vertex_groups(Operator):
|
||||
"""Delete vertex groups which are not used by any modifiers, deforming bones, shape keys, or constraints"""
|
||||
|
||||
bl_idname = "object.delete_unused_vgroups"
|
||||
bl_label = "Delete Unused Vertex Groups"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_deformed_mesh_with_vgroups(cls, context, must_deform=False)
|
||||
|
||||
def execute(self, context):
|
||||
deleted_names = delete_unused_vgroups(context.active_object)
|
||||
|
||||
self.report({'INFO'}, f"Deleted {len(deleted_names)} unused non-deform groups.")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def delete_unused_vgroups(mesh_ob: Object) -> list[str]:
|
||||
"""Returns a list of vertex group names that got deleted."""
|
||||
groups_to_delete = get_unused_vgroups(mesh_ob)
|
||||
|
||||
names = [vgroup.name for vgroup in groups_to_delete]
|
||||
print(f"Deleting unused non-deform groups:")
|
||||
print(" " + "\n ".join(names))
|
||||
|
||||
delete_vgroups(mesh_ob, groups_to_delete)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def get_unused_vgroups(mesh_ob: Object) -> set[VertexGroup]:
|
||||
return set(mesh_ob.vertex_groups) - set(get_used_vgroups(mesh_ob))
|
||||
|
||||
|
||||
def get_used_vgroups(mesh_ob: Object) -> list[VertexGroup]:
|
||||
"""Get a list of vertex groups used by the object.
|
||||
|
||||
Currently accounts for Modifiers, Armatures, GeoNodes, Physics,
|
||||
Shape Keys, and Constraints of dependent objects.
|
||||
"""
|
||||
|
||||
used_vgroups = []
|
||||
# Inputs of Modifiers, including GeoNodes.
|
||||
for modifier in mesh_ob.modifiers:
|
||||
if modifier.type == 'NODES':
|
||||
print(modifier.name)
|
||||
used_vgroups.extend(get_vgroups_used_by_geonodes(mesh_ob, modifier))
|
||||
else:
|
||||
used_vgroups.extend(get_referenced_vgroups(mesh_ob, modifier))
|
||||
if modifier.type == 'ARMATURE':
|
||||
used_vgroups.extend(get_deforming_vgroups(mesh_ob, modifier.object))
|
||||
# Masks of Physics settings.
|
||||
if hasattr(modifier, 'settings'):
|
||||
used_vgroups.extend(get_referenced_vgroups(mesh_ob, modifier.settings))
|
||||
|
||||
# Masks of Shape Keys.
|
||||
used_vgroups.extend(get_vgroups_used_by_shape_keys(mesh_ob))
|
||||
|
||||
# Constraints of dependent objects.
|
||||
used_vgroups.extend(get_vgroups_used_by_constraints_of_dependent_objects(mesh_ob))
|
||||
|
||||
return used_vgroups
|
||||
|
||||
|
||||
def get_referenced_vgroups(mesh_ob: Object, py_ob: object) -> list[VertexGroup]:
|
||||
"""Return a list of vertex groups directly referenced by any of the PyObject's members.
|
||||
Note that this is NOT a recursive function, and it can't really become one.
|
||||
|
||||
Useful for determining if a vertex group is used by anything or not, but
|
||||
you still have to be thorough, and call this function on any sub-member
|
||||
of some object that might reference vertex groups."""
|
||||
referenced_vgroups = []
|
||||
for member in dir(py_ob):
|
||||
value = getattr(py_ob, member)
|
||||
if type(value) != str:
|
||||
continue
|
||||
vg = mesh_ob.vertex_groups.get(value)
|
||||
if vg:
|
||||
referenced_vgroups.append(vg)
|
||||
return referenced_vgroups
|
||||
|
||||
|
||||
def get_vgroups_used_by_shape_keys(mesh_ob) -> list[VertexGroup]:
|
||||
mask_vgroups = []
|
||||
if not mesh_ob.data.shape_keys:
|
||||
return mask_vgroups
|
||||
for key_block in mesh_ob.data.shape_keys.key_blocks:
|
||||
vgroup = mesh_ob.vertex_groups.get(key_block.vertex_group)
|
||||
if vgroup and vgroup.name not in mask_vgroups:
|
||||
mask_vgroups.append(vgroup)
|
||||
return mask_vgroups
|
||||
|
||||
|
||||
def get_vgroups_used_by_constraints_of_dependent_objects(
|
||||
mesh_ob: Object,
|
||||
) -> list[VertexGroup]:
|
||||
used_vgroups = []
|
||||
dependent_objs = [id for id in bpy.data.user_map()[mesh_ob] if type(id) == Object]
|
||||
for dependent_obj in dependent_objs:
|
||||
constraint_lists = [dependent_obj.constraints]
|
||||
if dependent_obj.type == 'ARMATURE':
|
||||
constraint_lists += [pb.constraints for pb in dependent_obj.pose.bones]
|
||||
|
||||
for constraint_list in constraint_lists:
|
||||
for constraint in constraint_list:
|
||||
if (
|
||||
hasattr(constraint, 'target')
|
||||
and hasattr(constraint, 'subtarget')
|
||||
and constraint.target == mesh_ob
|
||||
and constraint.subtarget
|
||||
):
|
||||
vgroup = mesh_ob.vertex_groups.get(constraint.subtarget)
|
||||
if vgroup:
|
||||
used_vgroups.append(vgroup)
|
||||
if constraint.space_object == mesh_ob and constraint.space_subtarget:
|
||||
vgroup = mesh_ob.vertex_groups.get(constraint.space_subtarget)
|
||||
if vgroup:
|
||||
used_vgroups.append(vgroup)
|
||||
return used_vgroups
|
||||
|
||||
|
||||
def get_vgroups_used_by_geonodes(mesh_ob: Object, modifier: Modifier) -> list[VertexGroup]:
|
||||
used_vgroups = []
|
||||
for identifier in geomod_get_input_identifiers(modifier):
|
||||
use_attrib = identifier + "_use_attribute"
|
||||
if use_attrib in modifier and modifier[use_attrib]:
|
||||
attrib_name = modifier[identifier + "_attribute_name"]
|
||||
if attrib_name in mesh_ob.vertex_groups:
|
||||
# NOTE: Could be a false positive if this is a non-vertexgroup attribute with a matching name.
|
||||
used_vgroups.append(mesh_ob.vertex_groups[attrib_name])
|
||||
return used_vgroups
|
||||
|
||||
|
||||
def geomod_get_input_identifiers(modifier: Modifier) -> set[str]:
|
||||
if hasattr(modifier.node_group, 'interface'):
|
||||
# 4.0
|
||||
return {
|
||||
socket.identifier
|
||||
for socket in modifier.node_group.interface.items_tree
|
||||
if socket.item_type == 'SOCKET'
|
||||
and socket.in_out == 'INPUT'
|
||||
and socket.socket_type != 'NodeSocketGeometry'
|
||||
}
|
||||
else:
|
||||
# 3.6
|
||||
return {input.identifier for input in modifier.node_group.inputs[1:]}
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_OT_delete_unused_vertex_groups]
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..utils import poll_weight_paint_mode, reveal_bone, get_deforming_vgroups
|
||||
|
||||
|
||||
class EASYWEIGHT_OT_focus_deform_bones(Operator):
|
||||
"""While in Weight Paint Mode, reveal the layers of, unhide, and select the bones of all deforming vertex groups"""
|
||||
|
||||
bl_idname = "object.focus_deform_vgroups"
|
||||
bl_label = "Focus Deforming Bones"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_weight_paint_mode(cls, context, with_rig=True, with_groups=True)
|
||||
|
||||
def execute(self, context):
|
||||
# Deselect all bones
|
||||
for pb in context.selected_pose_bones[:]:
|
||||
pb.bone.select = False
|
||||
|
||||
# Reveal and select all deforming pose bones.
|
||||
deform_groups = get_deforming_vgroups(context.active_object)
|
||||
rig = context.pose_object
|
||||
for vg in deform_groups:
|
||||
pb = rig.pose.bones.get(vg.name)
|
||||
if not pb:
|
||||
continue
|
||||
reveal_bone(pb.bone, select=True)
|
||||
|
||||
self.report({'INFO'}, "Un-hid and selected all deforming bones.")
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_OT_focus_deform_bones]
|
||||
@@ -0,0 +1,217 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from bpy.types import Operator, Object
|
||||
from bpy.props import EnumProperty
|
||||
from bpy.utils import flip_name
|
||||
from mathutils.kdtree import KDTree
|
||||
|
||||
from ..utils import poll_deformed_mesh_with_vgroups
|
||||
|
||||
|
||||
class EASYWEIGHT_OT_symmetrize_groups(Operator):
|
||||
"""Symmetrize weights of vertex groups on a near-symmetrical mesh.\May have poor results on assymetrical meshes"""
|
||||
|
||||
bl_idname = "object.symmetrize_vertex_weights"
|
||||
bl_label = "Symmetrize Vertex Weights"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
groups: EnumProperty(
|
||||
name="Groups",
|
||||
description="Subset of vertex groups that should be symmetrized",
|
||||
items=[
|
||||
('ACTIVE', 'Active', 'Active'),
|
||||
('SELECTED', 'Selected Bones', 'Selected Bones'),
|
||||
('ALL', 'All', 'All'),
|
||||
],
|
||||
)
|
||||
|
||||
direction: EnumProperty(
|
||||
name="Direction",
|
||||
description="Whether to symmetrize from left to right or from right to left",
|
||||
items=[
|
||||
('AUTOMATIC', "Automatic", "Determine symmetrizing direction by the names of source vertex groups"),
|
||||
('LEFT_TO_RIGHT', "Left to Right", "Left to Right"),
|
||||
('RIGHT_TO_LEFT', "Right to Left", "Right to Left"),
|
||||
],
|
||||
options={'SKIP_SAVE'},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return poll_deformed_mesh_with_vgroups(cls, context, must_deform=False)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
layout.prop(self, 'groups')
|
||||
layout.prop(self, 'direction')
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
|
||||
vgroups = [obj.vertex_groups.active]
|
||||
if self.groups == 'SELECTED':
|
||||
vgroups = []
|
||||
# Get vertex groups of selected bones.
|
||||
for pbone in context.selected_pose_bones:
|
||||
vgroup = obj.vertex_groups.get(pbone.name)
|
||||
if not vgroup:
|
||||
continue
|
||||
flipped_vg = flip_name(pbone.name)
|
||||
if flipped_vg in vgroups:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
f'Both sides selected: "{vgroup.name}" & "{flipped_vg.name}". Only one side should be selected.',
|
||||
)
|
||||
return {'CANCELLED'}
|
||||
vgroups.append(vgroup)
|
||||
|
||||
elif self.groups == 'ALL':
|
||||
vgroups = obj.vertex_groups[:]
|
||||
|
||||
symmetry_mapping = get_symmetry_mapping(obj=obj)
|
||||
|
||||
if self.direction == 'AUTOMATIC':
|
||||
self.direction = 'LEFT_TO_RIGHT'
|
||||
righties = 0
|
||||
lefties = 0
|
||||
for vgroup in vgroups:
|
||||
name = vgroup.name.lower()
|
||||
if (
|
||||
".r" in name
|
||||
or "_r" in name
|
||||
or "r_" in name
|
||||
or "right" in name
|
||||
):
|
||||
righties += 1
|
||||
if (
|
||||
".l" in name
|
||||
or "_l" in name
|
||||
or "l_" in name
|
||||
or "left" in name
|
||||
):
|
||||
lefties += 1
|
||||
if righties > lefties:
|
||||
self.direction = 'RIGHT_TO_LEFT'
|
||||
|
||||
for vgroup in vgroups:
|
||||
symmetrize_vertex_group(
|
||||
obj=obj,
|
||||
vg_name=vgroup.name,
|
||||
symmetry_mapping=symmetry_mapping,
|
||||
right_to_left=self.direction == 'RIGHT_TO_LEFT',
|
||||
)
|
||||
|
||||
msg_direction = self.direction.replace("_", " ").lower()
|
||||
self.report({'INFO'}, f"Symmetrized {len(vgroups)} groups {msg_direction}.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_symmetry_mapping(*, obj: Object, axis='X', symmetrize_pos_to_neg=False) -> dict[int, int]:
|
||||
"""
|
||||
Create a mapping of vertex indicies, such that the index on one side maps
|
||||
to the index on the opposite side of the mesh on a given axis.
|
||||
"""
|
||||
assert axis in 'XYZ', "Axis must be X, Y or Z!"
|
||||
vertices = obj.data.vertices
|
||||
|
||||
size = len(vertices)
|
||||
kd = KDTree(size)
|
||||
for i, v in enumerate(vertices):
|
||||
kd.insert(v.co, i)
|
||||
kd.balance()
|
||||
|
||||
coord_i = 'XYZ'.find(axis)
|
||||
|
||||
# Figure out the function that will be used to determine whether a vertex
|
||||
# should be skipped or not.
|
||||
def zero_or_more(x):
|
||||
return x >= 0
|
||||
|
||||
def zero_or_less(x):
|
||||
return x <= 0
|
||||
|
||||
skip_func = zero_or_more if symmetrize_pos_to_neg else zero_or_less
|
||||
|
||||
# For any vertex with an X coordinate > 0, try to find a vertex at
|
||||
# the coordinate with X flipped.
|
||||
vert_map = {}
|
||||
bad_counter = 0
|
||||
for vert_idx, vert in enumerate(vertices):
|
||||
if abs(vert.co[coord_i]) < 0.0001:
|
||||
vert_map[vert_idx] = vert_idx
|
||||
continue
|
||||
# if skip_func(vert.co[coord_i]):
|
||||
# continue
|
||||
flipped_co = vert.co.copy()
|
||||
flipped_co[coord_i] *= -1
|
||||
_opposite_co, opposite_idx, dist = kd.find(flipped_co)
|
||||
if dist > 0.1: # pretty big threshold, for testing.
|
||||
bad_counter += 1
|
||||
continue
|
||||
if opposite_idx in vert_map.values():
|
||||
# This vertex was already mapped, and another vertex just matched with it.
|
||||
# No way to tell which is correct. Input mesh should just be more symmetrical.
|
||||
bad_counter += 1
|
||||
continue
|
||||
vert_map[vert_idx] = opposite_idx
|
||||
return vert_map
|
||||
|
||||
|
||||
def symmetrize_vertex_group(
|
||||
*, obj: Object, vg_name: str, symmetry_mapping: dict[int, int], right_to_left=False
|
||||
):
|
||||
"""
|
||||
Symmetrize weights of a single group. The symmetry_mapping should first be
|
||||
calculated with get_symmetry_mapping().
|
||||
"""
|
||||
|
||||
vgroup = obj.vertex_groups.get(vg_name)
|
||||
if not vgroup:
|
||||
return
|
||||
opp_name = flip_name(vg_name)
|
||||
opp_vgroup = obj.vertex_groups.get(opp_name)
|
||||
if not opp_vgroup:
|
||||
opp_vgroup = obj.vertex_groups.new(name=opp_name)
|
||||
|
||||
skip_func = None
|
||||
if vgroup != opp_vgroup:
|
||||
# Clear weights of the opposite group from all vertices.
|
||||
opp_vgroup.remove(range(len(obj.data.vertices)))
|
||||
else:
|
||||
# If the name isn't flippable, only remove weights of vertices
|
||||
# whose X coord >= 0.
|
||||
|
||||
# Figure out the function that will be used to determine whether a vertex
|
||||
# should be skipped or not.
|
||||
def zero_or_more(x):
|
||||
return x >= 0
|
||||
|
||||
def zero_or_less(x):
|
||||
return x <= 0
|
||||
|
||||
skip_func = zero_or_more if right_to_left else zero_or_less
|
||||
|
||||
# Write the new, mirrored weights
|
||||
for src_idx, dst_idx in symmetry_mapping.items():
|
||||
vert = obj.data.vertices[src_idx]
|
||||
if skip_func != None and skip_func(vert.co.x):
|
||||
continue
|
||||
try:
|
||||
src_weight = vgroup.weight(src_idx)
|
||||
if src_weight == 0:
|
||||
continue
|
||||
except RuntimeError:
|
||||
continue
|
||||
opp_vgroup.add([dst_idx], src_weight, 'REPLACE')
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_OT_symmetrize_groups]
|
||||
Reference in New Issue
Block a user