Add Easy_Weight
to Addons
#47
17
README.md
17
README.md
@ -27,17 +27,24 @@ _(The "OK" button is not needed, I just can't avoid it)_
|
|||||||
|
|
||||||
This panel provides quick access to commonly needed tools, whether they are part of core Blender or the addon:
|
This panel provides quick access to commonly needed tools, whether they are part of core Blender or the addon:
|
||||||
- Global toggles for the Accumulate, Front Faces Only and Falloff Shape brush options.
|
- Global toggles for the Accumulate, Front Faces Only and Falloff Shape brush options.
|
||||||
- Weight Paint mode settings including new ["Clean Weights"](#clean-weights) option
|
- Weight Paint mode settings including a new "Clean Weights" option to run the Clean Weights operator after every brush stroke.
|
||||||
- Commonly used Overlay and Armature display settings
|
- Commonly used Overlay and Armature display settings.
|
||||||
- Commonly used or [new](#vertex-group-operators) operators
|
- Commonly used or [new](#vertex-group-operators) operators.
|
||||||
It lets you change the brush falloff type (Sphere/Projected) and the Front Faces Only option. These will affect ALL brushes.
|
It lets you change the brush falloff type (Sphere/Projected) and the Front Faces Only option. These will affect ALL brushes.
|
||||||
Also, the color of your brushes will be darker when you're using Sphere falloff.
|
Also, the color of your brushes will be darker when you're using Sphere falloff.
|
||||||
|
|
||||||
I recommend to overwrite the shortcut of the default weight paint context menu like so:
|
I recommend to overwrite the shortcut of the default weight paint context menu like so:
|
||||||
<img src="docs/wp_context_menu_shortcut.png" width="500" />
|
<img src="docs/wp_context_menu_shortcut.png" width="500" />
|
||||||
|
|
||||||
### Clean Weights
|
### Hunting Rogue Weights
|
||||||
This is a new functionality found in the custom WP context menu. When enabled, **this will run the Clean Vertex Groups operator after every brush stroke** while you're in weight paint mode. This means 0-weights are automatically removed as they appear, which helps avoid small weight islands and rogue weights appearing as you work.
|
The addon provides a super sleek workflow for hunting down rogue weights efficiently but safely, with just the right amount of automation. This functionality can be found in the Sidebar->EasyWeight->Weight Islands panel.
|
||||||
|
<img src="docs/weight_islands.png" width="500" />
|
||||||
|
|
||||||
|
- After pressing Calculate Weight Islands and waiting a few seconds, you will see a list of all vertex groups which consist of more than a single island.
|
||||||
|
- Clicking the magnifying glass icon will focus the smallest island in the group, so you can decide what to do with it.
|
||||||
|
- If the island is rogue weights, you can subtract them and go back to the previous step. If not, you can press the checkmark icon next to the magnifying glass, and the vertex group will be hidden from the list.
|
||||||
|
- Continue with this process until all entries are gone from the list.
|
||||||
|
- In the end, you can be 100% sure that you have no rogue weights anywhere on your mesh.
|
||||||
|
|
||||||
### Vertex Group Operators
|
### Vertex Group Operators
|
||||||
The Vertex Groups context menu is re-organized with more icons and better labels, as well as some additional operators:
|
The Vertex Groups context menu is re-organized with more icons and better labels, as well as some additional operators:
|
||||||
|
BIN
docs/weight_islands.png
Normal file
BIN
docs/weight_islands.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
373
rogue_weights.py
373
rogue_weights.py
@ -1,14 +1,32 @@
|
|||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import bpy, sys
|
import bpy, sys
|
||||||
from bpy.types import Operator, Mesh, VertexGroup, MeshVertex, Object
|
from bpy.props import IntProperty, CollectionProperty, PointerProperty, StringProperty, BoolProperty
|
||||||
|
from bpy.types import (PropertyGroup, Panel, UIList, Operator,
|
||||||
|
Mesh, VertexGroup, MeshVertex, Object)
|
||||||
import itertools
|
import itertools
|
||||||
import bmesh
|
import bmesh
|
||||||
|
|
||||||
from .vertex_group_operators import WeightPaintOperator
|
from .vertex_group_operators import get_deforming_armature, get_deforming_vgroups
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module creates a workflow for hunting down and cleaning up rogue weights in the most efficient way possible.
|
||||||
|
All functionality can be found in the Sidebar->EasyWeight->Weight Islands panel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# UIList: Filtering options, explanations as to what the numbers mean. Maybe a warning for Calculate Islands operator when the mesh has a lot of verts or vgroups.
|
||||||
|
|
||||||
def build_vert_index_map(mesh) -> dict:
|
def build_vert_index_map(mesh) -> dict:
|
||||||
"""Build a dictionary of vertex indicies pointing to a list of other vertex indicies that the vertex is connected to by an edge."""
|
"""Build a dictionary of vertex indicies pointing to a list of other vertex indicies that the vertex is connected to by an edge."""
|
||||||
|
|
||||||
|
assert bpy.context.mode == 'EDIT_MESH'
|
||||||
|
|
||||||
|
bpy.ops.mesh.select_mode(type='VERT')
|
||||||
|
bpy.ops.mesh.reveal()
|
||||||
|
bpy.ops.mesh.select_all(action='SELECT')
|
||||||
|
bpy.ops.object.vertex_group_clean(group_select_mode='ALL', limit=0, keep_single=False)
|
||||||
|
|
||||||
bm = bmesh.from_edit_mesh(mesh)
|
bm = bmesh.from_edit_mesh(mesh)
|
||||||
v_dict = {}
|
v_dict = {}
|
||||||
for vert in bm.verts:
|
for vert in bm.verts:
|
||||||
@ -28,13 +46,19 @@ def find_weight_island_vertices(mesh: Mesh, vert_idx: int, group_index: int, ver
|
|||||||
if connected_vert_idx in island: # Avoid infinite recursion!
|
if connected_vert_idx in island: # Avoid infinite recursion!
|
||||||
continue
|
continue
|
||||||
for g in mesh.vertices[connected_vert_idx].groups: # For each group this other vertex belongs to
|
for g in mesh.vertices[connected_vert_idx].groups: # For each group this other vertex belongs to
|
||||||
if g.group == group_index and g.weight > 0: # If this vert is in the group
|
if g.group == group_index and g.weight: # If this vert is in the group
|
||||||
find_weight_island_vertices(mesh, connected_vert_idx, group_index, vert_idx_map, island) # Continue recursion
|
find_weight_island_vertices(mesh, connected_vert_idx, group_index, vert_idx_map, island) # Continue recursion
|
||||||
return island
|
return island
|
||||||
|
|
||||||
def find_any_vertex_in_group(mesh: Mesh, vgroup: VertexGroup, excluded_indicies=[]) -> MeshVertex:
|
def find_any_vertex_in_group(mesh: Mesh, vgroup: VertexGroup, excluded_indicies=[]) -> MeshVertex:
|
||||||
"""Return the index of the first vertex we find which is part of the
|
"""Return the index of the first vertex we find which is part of the
|
||||||
vertex group and optinally, has a specified selection state."""
|
vertex group and optinally, has a specified selection state."""
|
||||||
|
|
||||||
|
# TODO: This is probably our performance bottleneck atm.
|
||||||
|
# We should build an acceleration structure for this similar to build_vert_index_map,
|
||||||
|
# to map each vertex group to all of the verts within it, so we only need to iterate
|
||||||
|
# like this once.
|
||||||
|
|
||||||
for v in mesh.vertices:
|
for v in mesh.vertices:
|
||||||
if v.index in excluded_indicies:
|
if v.index in excluded_indicies:
|
||||||
continue
|
continue
|
||||||
@ -43,111 +67,314 @@ def find_any_vertex_in_group(mesh: Mesh, vgroup: VertexGroup, excluded_indicies=
|
|||||||
return v
|
return v
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def build_weight_islands_in_group(mesh: Mesh, vgroup: VertexGroup, vert_index_map: dict) -> List[List[int]]:
|
def get_islands_of_vgroup(mesh: Mesh, vgroup: VertexGroup, vert_index_map: dict) -> List[List[int]]:
|
||||||
"""Return a list of lists of vertex indicies: Weight islands within this vertex group."""
|
"""Return a list of lists of vertex indicies: Weight islands within this vertex group."""
|
||||||
islands = []
|
islands = []
|
||||||
while True:
|
while True:
|
||||||
flat_islands = set(itertools.chain.from_iterable(islands))
|
flat_islands = set(itertools.chain.from_iterable(islands))
|
||||||
any_unselected_vertex_in_group = find_any_vertex_in_group(mesh, vgroup, excluded_indicies=flat_islands)
|
any_vert_in_group = find_any_vertex_in_group(mesh, vgroup, excluded_indicies=flat_islands)
|
||||||
if not any_unselected_vertex_in_group:
|
if not any_vert_in_group:
|
||||||
break
|
break
|
||||||
sys.setrecursionlimit(len(mesh.vertices)) # TODO: I guess recursion is bad and we should avoid it here?
|
sys.setrecursionlimit(len(mesh.vertices)) # TODO: I guess recursion is bad and we should avoid it here? (we would just do the expand in a while True, and break if the current list of verts is the same as at the end of the last loop, no recursion involved.)
|
||||||
island = find_weight_island_vertices(mesh, any_unselected_vertex_in_group.index, vgroup.index, vert_index_map, island=[])
|
island = find_weight_island_vertices(mesh, any_vert_in_group.index, vgroup.index, vert_index_map, island=[])
|
||||||
sys.setrecursionlimit(990)
|
sys.setrecursionlimit(990)
|
||||||
islands.append(island)
|
islands.append(island)
|
||||||
return islands
|
return islands
|
||||||
|
|
||||||
|
def update_vgroup_islands(mesh, vgroup, vert_index_map, island_groups, island_group=None) -> IslandGroup:
|
||||||
|
islands = get_islands_of_vgroup(mesh, vgroup, vert_index_map)
|
||||||
|
|
||||||
|
if not island_group:
|
||||||
|
island_group = island_groups.add()
|
||||||
|
island_group.index = len(island_groups)-1
|
||||||
|
island_group.name = vgroup.name
|
||||||
|
else:
|
||||||
|
island_group.islands.clear()
|
||||||
|
for island in islands:
|
||||||
|
island_storage = island_group.islands.add()
|
||||||
|
for v_idx in island:
|
||||||
|
v_idx_storage = island_storage.vert_indicies.add()
|
||||||
|
v_idx_storage.index = v_idx
|
||||||
|
|
||||||
|
return island_group
|
||||||
|
|
||||||
def select_vertices(mesh: Mesh, vert_indicies: List[int]):
|
def select_vertices(mesh: Mesh, vert_indicies: List[int]):
|
||||||
assert bpy.context.mode != 'EDIT_MESH', "Object must not be in edit mode, otherwise vertex selection doesn't work!"
|
assert bpy.context.mode != 'EDIT_MESH', "Object must not be in edit mode, otherwise vertex selection doesn't work!"
|
||||||
for vi in vert_indicies:
|
for vi in vert_indicies:
|
||||||
mesh.vertices[vi].select = True
|
mesh.vertices[vi].select = True
|
||||||
|
|
||||||
# TODO: This needs to be split off into a separate, even smarter system with more UI:
|
class VertIndex(PropertyGroup):
|
||||||
# It should be like the error checking buttons in the 3D Print addon by Campbell:
|
index: IntProperty()
|
||||||
# You press the operator, and it detects and SAVES all the issues that were found:
|
|
||||||
# There is a sidebar panel for this (probably along with other EasyWeight UI)
|
|
||||||
# Pressing the button populates a UIList with vertex groups that were found with multiple islands.
|
|
||||||
# Selecting an entry in the UIList brings up ANOTHER UIList with each island and the number of vertices in them.
|
|
||||||
# A vertex group can be marked as correct, which will save the number of islands it had in the moment when it was considered correct, so on subsequent runs of the operator, it will be ignored as long as it has that number of islands still.
|
|
||||||
# Each island can be selected with a button (also selects the corresponding pose bone)
|
|
||||||
class FocusRogueDeformingWeights(WeightPaintOperator):
|
|
||||||
"""While in weight paint mode, find and focus a deforming vertex group which consists of several islands. The smallest weight island is selected, but no weights are removed. Keep running this until no more rogue weights are found"""
|
|
||||||
bl_idname = "object.focus_rogue_weights"
|
|
||||||
bl_label = "Focus Rogue Weights"
|
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
|
||||||
|
|
||||||
@staticmethod
|
class WeightIsland(PropertyGroup):
|
||||||
def save_skip_group(obj, vgroup):
|
vert_indicies: CollectionProperty(type=VertIndex) # TODO: Is this really needed?? Why can't a CollectionProperty(type=IntProperty) be fine??
|
||||||
"""Store this group's name in a custom property so it can be skipped on subsequent runs."""
|
|
||||||
if 'skip_groups' not in obj:
|
|
||||||
obj['skip_groups'] = []
|
|
||||||
l = obj['skip_groups']
|
|
||||||
if type(l) != list:
|
|
||||||
l = l.to_list()
|
|
||||||
l.append(vgroup.name)
|
|
||||||
obj['skip_groups'] = l
|
|
||||||
|
|
||||||
@staticmethod
|
class IslandGroup(PropertyGroup):
|
||||||
def find_rogue_deform_weights(obj: Object, vert_index_map: dict) -> Tuple[VertexGroup, List[List[int]]]:
|
name: StringProperty() # Name of the vertex group this set of island is associated with
|
||||||
"""Return the first vertex group we find that has multiple islands, as well as the islands."""
|
islands: CollectionProperty(type=WeightIsland)
|
||||||
|
num_expected_islands: IntProperty(
|
||||||
|
name="Expected Islands",
|
||||||
|
default=1,
|
||||||
|
min=1,
|
||||||
|
description="Number of weight islands that have been marked as the expected amount by the user. If the real amount differs from this value, a warning appears"
|
||||||
|
)
|
||||||
|
index: IntProperty()
|
||||||
|
|
||||||
|
class MarkIslandsAsOkay(Operator):
|
||||||
|
"""Mark this number of vertex islands to be the intended amount. Vertex group will be hidden from the list until this number changes"""
|
||||||
|
bl_idname = "object.set_expected_island_count"
|
||||||
|
bl_label = "Set Intended Island Count"
|
||||||
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||||
|
|
||||||
|
vgroup: StringProperty(
|
||||||
|
name="Vertex Group",
|
||||||
|
default="",
|
||||||
|
description="Name of the vertex group whose intended island count will be set"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
rig = context.pose_object
|
||||||
|
obj = context.object
|
||||||
mesh = obj.data
|
mesh = obj.data
|
||||||
|
org_mode = obj.mode
|
||||||
|
|
||||||
|
assert self.vgroup in obj.island_groups, f"Island Group {self.vgroup} not found in object {obj.name}, aborting."
|
||||||
|
|
||||||
|
# Update existing island data first
|
||||||
|
island_group = obj.island_groups[self.vgroup]
|
||||||
|
vgroup = obj.vertex_groups[self.vgroup]
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
vert_index_map = build_vert_index_map(mesh)
|
||||||
|
bpy.ops.object.mode_set(mode=org_mode)
|
||||||
|
org_num_islands = len(island_group.islands)
|
||||||
|
island_group = update_vgroup_islands(mesh, vgroup, vert_index_map, obj.island_groups, island_group)
|
||||||
|
new_num_islands = len(island_group.islands)
|
||||||
|
if new_num_islands != org_num_islands:
|
||||||
|
if new_num_islands == 1:
|
||||||
|
self.report({'INFO'}, f"Vertex group is now a single island, changing expected island count no longer necessary.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
self.report({'INFO'}, f"Vertex group island count changed from {org_num_islands} to {new_num_islands}. Click again to mark this as the expected number.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
island_group.num_expected_islands = new_num_islands
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class FocusSmallestIsland(Operator):
|
||||||
|
"""Enter Weight Paint mode and focus on the smallest island"""
|
||||||
|
bl_idname = "object.focus_smallest_weight_island"
|
||||||
|
bl_label = "Focus Smallest Island"
|
||||||
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||||
|
|
||||||
|
enter_wp: BoolProperty(
|
||||||
|
name="Enter Weight Paint",
|
||||||
|
default=True,
|
||||||
|
description="Enter Weight Paint Mode using the Toggle Weight Paint operator"
|
||||||
|
)
|
||||||
|
vgroup: StringProperty(
|
||||||
|
name="Vertex Group",
|
||||||
|
default="",
|
||||||
|
description="Name of the vertex group whose smallest island should be focused"
|
||||||
|
)
|
||||||
|
focus_view: BoolProperty(
|
||||||
|
name="Focus View",
|
||||||
|
default=True,
|
||||||
|
description="Whether to focus the 3D Viewport on the selected vertices"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
rig = context.pose_object
|
||||||
|
obj = context.object
|
||||||
|
mesh = obj.data
|
||||||
|
org_mode = obj.mode
|
||||||
|
|
||||||
|
assert self.vgroup in obj.vertex_groups, f"Vertex Group {self.vgroup} not found in object {obj.name}, aborting."
|
||||||
|
|
||||||
|
if self.vgroup in obj.island_groups:
|
||||||
|
# Update existing island data first
|
||||||
|
island_group = obj.island_groups[self.vgroup]
|
||||||
|
vgroup = obj.vertex_groups[self.vgroup]
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
vert_index_map = build_vert_index_map(mesh)
|
||||||
|
bpy.ops.object.mode_set(mode=org_mode)
|
||||||
|
org_num_islands = len(island_group.islands)
|
||||||
|
island_group = update_vgroup_islands(mesh, vgroup, vert_index_map, obj.island_groups, island_group)
|
||||||
|
new_num_islands = len(island_group.islands)
|
||||||
|
if new_num_islands != org_num_islands:
|
||||||
|
if new_num_islands == 1:
|
||||||
|
self.report({'INFO'}, f"Vertex group is now a single island, hidden from list.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
self.report({'INFO'}, f"Vertex group island count changed from {org_num_islands} to {new_num_islands}. Click again to focus smallest island.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
if org_mode != 'EDIT':
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.mesh.select_mode(type='VERT')
|
||||||
|
bpy.ops.mesh.reveal()
|
||||||
|
bpy.ops.mesh.select_all(action='DESELECT')
|
||||||
|
|
||||||
|
if org_mode != 'EDIT':
|
||||||
|
bpy.ops.object.mode_set(mode=org_mode)
|
||||||
|
else:
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
island_groups = obj.island_groups
|
||||||
|
island_group = island_groups[self.vgroup]
|
||||||
|
vgroup = obj.vertex_groups[self.vgroup]
|
||||||
|
obj.active_islands_index = island_group.index
|
||||||
|
obj.vertex_groups.active_index = vgroup.index
|
||||||
|
|
||||||
|
smallest_island = min(island_group.islands, key=lambda island: len(island.vert_indicies))
|
||||||
|
select_vertices(mesh, [vi.index for vi in smallest_island.vert_indicies])
|
||||||
|
|
||||||
|
if self.focus_view:
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
bpy.ops.view3d.view_selected()
|
||||||
|
bpy.ops.object.mode_set(mode=org_mode)
|
||||||
|
|
||||||
|
if self.enter_wp and org_mode != 'WEIGHT_PAINT':
|
||||||
|
bpy.ops.object.weight_paint_toggle()
|
||||||
|
|
||||||
|
mesh.use_paint_mask_vertex = True
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class CalculateWeightIslands(Operator):
|
||||||
|
"""Detect number of weight islands for each deforming vertex group"""
|
||||||
|
bl_idname = "object.calculate_weight_islands"
|
||||||
|
bl_label = "Calculate Weight Islands"
|
||||||
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def store_all_weight_islands(obj: Object, vert_index_map: dict):
|
||||||
|
"""Store the weight island information of every deforming vertex group."""
|
||||||
|
mesh = obj.data
|
||||||
|
island_groups = obj.island_groups
|
||||||
|
island_groups.clear() # TODO: This is bad, we need to hold onto num_expected_islands.
|
||||||
|
obj.active_islands_index = 0
|
||||||
for vgroup in get_deforming_vgroups(obj):
|
for vgroup in get_deforming_vgroups(obj):
|
||||||
if 'skip_groups' in obj and vgroup.name in obj['skip_groups']:
|
if 'skip_groups' in obj and vgroup.name in obj['skip_groups']:
|
||||||
continue
|
continue
|
||||||
obj.vertex_groups.active_index = vgroup.index
|
obj.vertex_groups.active_index = vgroup.index
|
||||||
|
|
||||||
islands = build_weight_islands_in_group(mesh, vgroup, vert_index_map)
|
update_vgroup_islands(mesh, vgroup, vert_index_map, island_groups)
|
||||||
|
|
||||||
if len(islands) > 1:
|
|
||||||
return vgroup, islands
|
|
||||||
|
|
||||||
return None, None
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
if not context.object or context.object.type != 'MESH':
|
||||||
|
return False
|
||||||
|
return context.mode != 'EDIT_MESH'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
rig = context.pose_object
|
|
||||||
obj = context.object
|
obj = context.object
|
||||||
|
rig = get_deforming_armature(obj)
|
||||||
|
org_mode = obj.mode
|
||||||
|
|
||||||
|
# TODO: Is it better to have this here instead of poll()?
|
||||||
|
assert rig, "Error: Object must be deformed by an armature, otherwise we can not tell which vertex groups are deforming."
|
||||||
|
|
||||||
org_vg_idx = obj.vertex_groups.active_index
|
org_vg_idx = obj.vertex_groups.active_index
|
||||||
org_mode = obj.mode
|
org_mode = obj.mode
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
bpy.ops.mesh.select_mode(type='VERT')
|
|
||||||
bpy.ops.mesh.reveal()
|
|
||||||
bpy.ops.mesh.select_all(action='DESELECT')
|
|
||||||
|
|
||||||
mesh = obj.data
|
mesh = obj.data
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
vert_index_map = build_vert_index_map(mesh)
|
vert_index_map = build_vert_index_map(mesh)
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
vgroup, islands = self.find_rogue_deform_weights(obj, vert_index_map)
|
self.store_all_weight_islands(obj, vert_index_map)
|
||||||
|
|
||||||
if vgroup:
|
|
||||||
# Select the smallest island.
|
|
||||||
select_vertices(mesh, min(islands, key=len))
|
|
||||||
|
|
||||||
# Support the case where the user chooses not to fix the rogue weights: Perhaps they are intentional
|
|
||||||
self.save_skip_group(obj, vgroup)
|
|
||||||
|
|
||||||
self.report({'INFO'}, f'Found Vertex Group "{vgroup.name}" with {len(islands)} islands. Fix or ignore it, then keep running this operator to find the next group with rogue weights.')
|
|
||||||
|
|
||||||
if rig:
|
|
||||||
rig.select_set(True)
|
|
||||||
mesh.use_paint_mask_vertex = True
|
|
||||||
bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
|
|
||||||
else:
|
|
||||||
if rig:
|
|
||||||
rig.select_set(True)
|
|
||||||
obj.vertex_groups.active_index = org_vg_idx
|
|
||||||
bpy.ops.object.mode_set(mode=org_mode)
|
|
||||||
if 'skip_groups' in obj and len(obj['skip_groups']) > 0:
|
|
||||||
self.report({'INFO'}, f"No rogue weights found, but {len(obj['skip_groups'])} were skipped. Keep running the operator to cycle through groups with multiple weight islands to make sure they are desired.")
|
|
||||||
del obj['skip_groups']
|
|
||||||
else:
|
|
||||||
self.report({'INFO'}, "No rogue weights found!")
|
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode=org_mode)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
class EASYWEIGHT_PT_WeightIslands(Panel):
|
||||||
|
"""Panel with utilities for detecting rogue weights."""
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'UI'
|
||||||
|
bl_category = 'EasyWeight'
|
||||||
|
bl_label = "Weight Islands"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type=='MESH'
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.operator(CalculateWeightIslands.bl_idname)
|
||||||
|
|
||||||
|
obj = context.object
|
||||||
|
island_groups = obj.island_groups
|
||||||
|
if len(island_groups)==0: return
|
||||||
|
active_weight_islands = obj.island_groups[obj.active_islands_index]
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
|
||||||
|
row.template_list(
|
||||||
|
'EASYWEIGHT_UL_weight_island_groups',
|
||||||
|
'',
|
||||||
|
obj,
|
||||||
|
'island_groups',
|
||||||
|
obj,
|
||||||
|
'active_islands_index',
|
||||||
|
)
|
||||||
|
|
||||||
|
class EASYWEIGHT_UL_weight_island_groups(UIList):
|
||||||
|
def filter_items(self, context, data, propname):
|
||||||
|
flt_flags = []
|
||||||
|
flt_neworder = []
|
||||||
|
list_items = getattr(data, propname)
|
||||||
|
|
||||||
|
island_groups = getattr(data, propname)
|
||||||
|
|
||||||
|
helper_funcs = bpy.types.UI_UL_list
|
||||||
|
|
||||||
|
if self.filter_name:
|
||||||
|
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, island_groups, "name",
|
||||||
|
reverse=self.use_filter_name_reverse)
|
||||||
|
|
||||||
|
if not flt_flags:
|
||||||
|
flt_flags = [self.bitflag_filter_item] * len(island_groups)
|
||||||
|
|
||||||
|
if self.use_filter_invert:
|
||||||
|
for idx, flag in enumerate(flt_flags):
|
||||||
|
flt_flags[idx] = 0 if flag else self.bitflag_filter_item
|
||||||
|
|
||||||
|
for idx, island_group in enumerate(island_groups):
|
||||||
|
if len(island_group.islands) < 1:
|
||||||
|
# Filter island groups with only 1 or 0 islands in them
|
||||||
|
flt_flags[idx] = 0
|
||||||
|
elif len(island_group.islands) == island_group.num_expected_islands:
|
||||||
|
# Filter island groups with the expected number of islands in them
|
||||||
|
flt_flags[idx] = 0
|
||||||
|
|
||||||
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
|
island_group = item
|
||||||
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
||||||
|
icon = 'ERROR'
|
||||||
|
num_islands = len(island_group.islands)
|
||||||
|
if num_islands == island_group.num_expected_islands:
|
||||||
|
icon = 'CHECKMARK'
|
||||||
|
row = layout.row()
|
||||||
|
row.label(text=island_group.name)
|
||||||
|
row.label(text=str(num_islands), icon=icon)
|
||||||
|
op = row.operator(FocusSmallestIsland.bl_idname, text="", icon='VIEWZOOM').vgroup = island_group.name
|
||||||
|
row.operator(MarkIslandsAsOkay.bl_idname, text="", icon='CHECKMARK').vgroup = island_group.name
|
||||||
|
# TODO: Operator to mark current number of islands as the expected amount
|
||||||
|
elif self.layout_type in {'GRID'}:
|
||||||
|
pass
|
||||||
|
|
||||||
classes = [
|
classes = [
|
||||||
FocusRogueDeformingWeights
|
VertIndex,
|
||||||
|
WeightIsland,
|
||||||
|
IslandGroup,
|
||||||
|
|
||||||
|
CalculateWeightIslands,
|
||||||
|
FocusSmallestIsland,
|
||||||
|
MarkIslandsAsOkay,
|
||||||
|
|
||||||
|
EASYWEIGHT_PT_WeightIslands,
|
||||||
|
EASYWEIGHT_UL_weight_island_groups
|
||||||
]
|
]
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
@ -155,6 +382,9 @@ def register():
|
|||||||
for c in classes:
|
for c in classes:
|
||||||
register_class(c)
|
register_class(c)
|
||||||
|
|
||||||
|
Object.island_groups = CollectionProperty(type=IslandGroup)
|
||||||
|
Object.active_islands_index = IntProperty()
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
from bpy.utils import unregister_class
|
from bpy.utils import unregister_class
|
||||||
for c in classes:
|
for c in classes:
|
||||||
@ -162,3 +392,6 @@ def unregister():
|
|||||||
unregister_class(c)
|
unregister_class(c)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass # TODO: Sometimes fails to unregister for literally no reason.
|
pass # TODO: Sometimes fails to unregister for literally no reason.
|
||||||
|
|
||||||
|
del Object.island_groups
|
||||||
|
del Object.active_islands_index
|
@ -2,7 +2,6 @@ import bpy
|
|||||||
from bpy.props import BoolProperty, EnumProperty
|
from bpy.props import BoolProperty, EnumProperty
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
from .vertex_group_operators import DeleteEmptyDeformGroups, DeleteUnusedVertexGroups
|
from .vertex_group_operators import DeleteEmptyDeformGroups, DeleteUnusedVertexGroups
|
||||||
from .rogue_weights import FocusRogueDeformingWeights
|
|
||||||
|
|
||||||
class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
|
class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
|
||||||
""" Custom Weight Paint context menu """
|
""" Custom Weight Paint context menu """
|
||||||
@ -62,8 +61,6 @@ class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
|
|||||||
row.operator(DeleteEmptyDeformGroups.bl_idname, text="Wipe Empty", icon='GROUP_BONE')
|
row.operator(DeleteEmptyDeformGroups.bl_idname, text="Wipe Empty", icon='GROUP_BONE')
|
||||||
row.operator(DeleteUnusedVertexGroups.bl_idname, text="Wipe Unused", icon='BRUSH_DATA')
|
row.operator(DeleteUnusedVertexGroups.bl_idname, text="Wipe Unused", icon='BRUSH_DATA')
|
||||||
|
|
||||||
layout.operator(FocusRogueDeformingWeights.bl_idname, icon='ZOOM_IN')
|
|
||||||
|
|
||||||
def draw_minimal(self, layout, context):
|
def draw_minimal(self, layout, context):
|
||||||
overlay = context.space_data.overlay
|
overlay = context.space_data.overlay
|
||||||
row = layout.row(heading="Symmetry: ")
|
row = layout.row(heading="Symmetry: ")
|
||||||
|
Loading…
Reference in New Issue
Block a user