EasyWeight: Major update #317

Merged
Demeter Dzadik merged 9 commits from easyweight-updates into main 2024-06-25 21:09:03 +02:00
5 changed files with 238 additions and 279 deletions
Showing only changes of commit 990d63de2c - Show all commits

View File

@ -1,9 +1,9 @@
from . import (
force_apply_mirror,
toggle_weight_paint,
weight_paint_context_menu,
vertex_group_operators,
weight_cleaner,
weight_pie,
vertex_group_menu,
rogue_weights,
prefs,
@ -27,9 +27,9 @@ bl_info = {
modules = [
force_apply_mirror,
toggle_weight_paint,
weight_paint_context_menu,
vertex_group_operators,
weight_cleaner,
weight_pie,
vertex_group_menu,
rogue_weights,
prefs,

View File

@ -38,6 +38,44 @@ class EASYWEIGHT_addon_preferences(bpy.types.AddonPreferences):
default=True,
)
def update_front_faces(self, context):
for brush in bpy.data.brushes:
if not brush.use_paint_weight:
continue
brush.use_frontface = self.global_front_faces_only
def update_accumulate(self, context):
for brush in bpy.data.brushes:
if not brush.use_paint_weight:
continue
brush.use_accumulate = self.global_accumulate
def update_falloff_shape(self, context):
for brush in bpy.data.brushes:
if not brush.use_paint_weight:
continue
brush.falloff_shape = 'SPHERE' if self.global_falloff_shape_sphere else 'PROJECTED'
for i, val in enumerate(brush.cursor_color_add):
if val > 0:
brush.cursor_color_add[i] = (
0.5 if self.global_falloff_shape_sphere else 2.0)
global_front_faces_only: BoolProperty(
name="Front Faces Only",
description="All weight brushes are able to paint on geometry that is facing away from the viewport",
update=update_front_faces
)
global_accumulate: BoolProperty(
name="Accumulate",
description="All weight paint brushes will accumulate their effect within a single stroke as you move the mouse",
update=update_accumulate
)
global_falloff_shape_sphere: BoolProperty(
name="Falloff Shape",
description="All weight paint brushes switch between a 3D spherical or a 2D projected circular falloff shape",
update=update_falloff_shape,
)
show_hotkeys: BoolProperty(
name="Show Hotkeys",
description="Reveal the hotkey list. You may customize or disable these hotkeys",
@ -171,6 +209,12 @@ class EASYWEIGHT_addon_preferences(bpy.types.AddonPreferences):
return kmi
def set_brush_prefs_on_file_load(scene):
prefs = get_addon_prefs()
prefs.global_front_faces_only = prefs.global_front_faces_only
prefs.global_accumulate = prefs.global_accumulate
prefs.global_falloff_shape_sphere = prefs.global_falloff_shape_sphere
# NOTE: This function is copied from CloudRig's cloudrig.py.
def register_hotkey(
bl_idname, hotkey_kwargs, *, key_cat='Window', space_type='EMPTY', op_kwargs={}
@ -234,15 +278,29 @@ registry = [EASYWEIGHT_addon_preferences]
def register():
register_hotkey(
bl_idname='object.custom_weight_paint_context_menu',
hotkey_kwargs={
'type': 'W',
'value': 'PRESS',
},
'wm.call_menu_pie',
hotkey_kwargs={'type': "W", 'value': "PRESS"},
key_cat='Weight Paint',
op_kwargs={'name': 'EASYWEIGHT_MT_PIE_easy_weight'},
)
register_hotkey(
bl_idname='object.weight_paint_toggle',
hotkey_kwargs={'type': 'TAB', 'value': 'PRESS', 'ctrl': True},
key_cat='3D View',
)
bpy.app.handlers.load_post.append(set_brush_prefs_on_file_load)
def unregister_hotkeys():
prefs_class = bpy.types.AddonPreferences.bl_rna_get_subclass_py('EASYWEIGHT_addon_preferences')
if not prefs_class:
return
for kmi_hash, kmi_tup in prefs_class.easyweight_keymap_items.items():
kc, km, kmi = kmi_tup
km.keymap_items.remove(kmi)
prefs_class.easyweight_keymap_items = {}
def unregister():
unregister_hotkeys()
bpy.app.handlers.load_post.remove(set_brush_prefs_on_file_load)

View File

@ -1,36 +1,28 @@
import bpy
from bpy.types import Menu
from .vertex_group_operators import (
EASYWEIGHTS_OT_delete_empty_deform_groups,
EASYWEIGHTS_OT_focus_deform_bones,
EASYWEIGHTS_OT_delete_unselected_deform_groups,
EASYWEIGHTS_OT_delete_unused_vertex_groups,
)
class MESH_MT_vertex_group_batch_delete(Menu):
bl_label = "Batch Delete"
def draw(self, context):
layout = self.layout
layout.operator("object.vertex_group_remove", text="All Groups", icon='TRASH').all = True
layout.operator(
"object.vertex_group_remove",
text="All Groups",
icon='TRASH'
).all = True
layout.operator(
"object.vertex_group_remove",
text="All Unlocked Groups",
icon='UNLOCKED'
"object.vertex_group_remove", text="All Unlocked Groups", icon='UNLOCKED'
).all_unlocked = True
layout.separator()
layout.operator(EASYWEIGHTS_OT_delete_empty_deform_groups.bl_idname,
text="Empty Deform Groups", icon='GROUP_BONE')
layout.operator(EASYWEIGHTS_OT_delete_unused_vertex_groups.bl_idname,
text="Unused Non-Deform Groups", icon='BRUSH_DATA')
layout.operator(EASYWEIGHTS_OT_delete_unselected_deform_groups.bl_idname,
text="Unselected Deform Groups", icon='RESTRICT_SELECT_ON')
layout.operator(
'object.delete_empty_deform_vgroups', text="Empty Deform Groups", icon='GROUP_BONE'
)
layout.operator(
'object.delete_unused_vgroups', text="Unused Non-Deform Groups", icon='BRUSH_DATA'
)
layout.operator(
'object.delete_unselected_deform_vgroups',
text="Unselected Deform Groups",
icon='RESTRICT_SELECT_ON',
)
class MESH_MT_vertex_group_symmetry(Menu):
@ -41,37 +33,29 @@ class MESH_MT_vertex_group_symmetry(Menu):
layout.operator(
"object.vertex_group_mirror",
text="Mirror Active Group (Proximity)",
icon='AUTOMERGE_OFF'
icon='AUTOMERGE_OFF',
).use_topology = False
layout.operator(
"object.vertex_group_mirror",
text="Mirror Active Group (Topology)",
icon='AUTOMERGE_ON'
"object.vertex_group_mirror", text="Mirror Active Group (Topology)", icon='AUTOMERGE_ON'
).use_topology = True
layout.separator()
layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize Active Group",
icon='MOD_MIRROR'
"object.symmetrize_vertex_weights", text="Symmetrize Active Group", icon='MOD_MIRROR'
).groups = 'ACTIVE'
layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize Selected Bones' Groups",
icon='MOD_MIRROR'
icon='MOD_MIRROR',
).groups = 'BONES'
op = layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize All Left->Right",
icon='MOD_MIRROR'
"object.symmetrize_vertex_weights", text="Symmetrize All Left->Right", icon='MOD_MIRROR'
)
op.groups = 'ALL'
op.direction = 'LEFT_TO_RIGHT'
op = layout.operator(
"object.symmetrize_vertex_weights",
text="Symmetrize All Right->Left",
icon='MOD_MIRROR'
"object.symmetrize_vertex_weights", text="Symmetrize All Right->Left", icon='MOD_MIRROR'
)
op.groups = 'ALL'
op.direction = 'RIGHT_TO_LEFT'
@ -100,15 +84,19 @@ class MESH_MT_vertex_group_copy(Menu):
def draw(self, context):
layout = self.layout
# TODO: This isn't grayed out when there's no active group.
# TODO: Maybe for things that use the active group, we should put the name of the group in the button text? Makes it harder to search tho perhaps. Not even sure if menu search supports dynamic menu text?
layout.operator("object.vertex_group_copy",
icon='DUPLICATE', text="Duplicate Group")
obj = context.object
if obj and obj.vertex_groups and obj.vertex_groups.active:
layout.operator(
"object.vertex_group_copy",
icon='DUPLICATE',
text=f'Duplicate "{obj.vertex_groups.active.name}"',
)
layout.separator()
layout.operator("object.vertex_group_copy_to_linked",
text="Synchronize Groups on All Instances", icon='LINKED')
layout.operator("object.vertex_group_copy_to_selected",
text="Synchronize Groups on Selected", icon='RESTRICT_SELECT_OFF')
layout.operator(
"object.vertex_group_copy_to_selected",
text="Synchronize Groups on Selected",
icon='RESTRICT_SELECT_OFF',
)
class MESH_MT_vertex_group_lock(Menu):
@ -117,14 +105,13 @@ class MESH_MT_vertex_group_lock(Menu):
def draw(self, context):
layout = self.layout
props = layout.operator(
"object.vertex_group_lock", icon='LOCKED', text="Lock All")
props = layout.operator("object.vertex_group_lock", icon='LOCKED', text="Lock All")
props.action, props.mask = 'LOCK', 'ALL'
props = layout.operator(
"object.vertex_group_lock", icon='UNLOCKED', text="Unlock All")
props = layout.operator("object.vertex_group_lock", icon='UNLOCKED', text="Unlock All")
props.action, props.mask = 'UNLOCK', 'ALL'
props = layout.operator(
"object.vertex_group_lock", icon='UV_SYNC_SELECT', text="Invert All Locks")
"object.vertex_group_lock", icon='UV_SYNC_SELECT', text="Invert All Locks"
)
props.action, props.mask = 'INVERT', 'ALL'
@ -140,9 +127,7 @@ class MESH_MT_vertex_group_weight(Menu):
text="Remove Selected Verts from All Groups",
).use_all_groups = True
layout.operator(
"object.vertex_group_clean",
icon='BRUSH_DATA',
text="Clean 0 Weights from All Groups"
"object.vertex_group_clean", icon='BRUSH_DATA', text="Clean 0 Weights from All Groups"
).group_select_mode = 'ALL'
layout.separator()
@ -150,20 +135,16 @@ class MESH_MT_vertex_group_weight(Menu):
layout.operator(
"object.vertex_group_remove_from",
icon='TRASH',
text="Remove All Verts from Selected Group"
text="Remove All Verts from Selected Group",
).use_all_verts = True
layout.separator()
layout.operator(
'paint.weight_from_bones',
text="Assign Automatic from Bones",
icon='BONE_DATA'
'paint.weight_from_bones', text="Assign Automatic from Bones", icon='BONE_DATA'
).type = 'AUTOMATIC'
op = layout.operator(
'object.vertex_group_normalize_all',
text="Normalize Deform",
icon='IPO_SINE'
'object.vertex_group_normalize_all', text="Normalize Deform", icon='IPO_SINE'
)
op.group_select_mode = 'BONE_DEFORM'
op.lock_active = False
@ -171,12 +152,7 @@ class MESH_MT_vertex_group_weight(Menu):
def draw_misc(self, context):
layout = self.layout
layout.operator(EASYWEIGHTS_OT_focus_deform_bones.bl_idname, icon='ZOOM_IN')
# TODO: Add an operator called "Smart Cleanup" that creates missing mirror groups,
# Cleans 0 weights,
# Deletes unused deforming groups,
# and deletes unused non-deforming groups.
layout.operator('object.focus_deform_vgroups', icon='ZOOM_IN')
def draw_vertex_group_menu(self, context):
@ -195,23 +171,27 @@ registry = [
MESH_MT_vertex_group_sort,
MESH_MT_vertex_group_copy,
MESH_MT_vertex_group_lock,
MESH_MT_vertex_group_weight
MESH_MT_vertex_group_weight,
]
def register():
bpy.types.MESH_MT_vertex_group_context_menu.old_draw = bpy.types.MESH_MT_vertex_group_context_menu.draw
bpy.types.MESH_MT_vertex_group_context_menu.old_draw = (
bpy.types.MESH_MT_vertex_group_context_menu.draw
)
bpy.types.MESH_MT_vertex_group_context_menu.remove(
bpy.types.MESH_MT_vertex_group_context_menu.draw)
bpy.types.MESH_MT_vertex_group_context_menu.draw
)
bpy.types.MESH_MT_vertex_group_context_menu.append(draw_vertex_group_menu)
bpy.types.MESH_MT_vertex_group_context_menu.append(draw_misc)
def unregister():
bpy.types.MESH_MT_vertex_group_context_menu.draw = bpy.types.MESH_MT_vertex_group_context_menu.old_draw
bpy.types.MESH_MT_vertex_group_context_menu.draw = (
bpy.types.MESH_MT_vertex_group_context_menu.old_draw
)
del bpy.types.MESH_MT_vertex_group_context_menu.old_draw
bpy.types.MESH_MT_vertex_group_context_menu.remove(draw_vertex_group_menu)
bpy.types.MESH_MT_vertex_group_context_menu.remove(draw_misc)

View File

@ -1,203 +0,0 @@
import bpy
from bpy.props import BoolProperty, EnumProperty
from .vertex_group_operators import EASYWEIGHTS_OT_delete_empty_deform_groups, EASYWEIGHTS_OT_delete_unused_vertex_groups
from .prefs import get_addon_prefs
class EASYWEIGHT_OT_wp_context_menu(bpy.types.Operator):
""" Custom Weight Paint context menu """
bl_idname = "object.custom_weight_paint_context_menu"
bl_label = "Custom Weight Paint Context Menu"
bl_options = {'REGISTER'}
def update_front_faces(self, context):
for b in bpy.data.brushes:
if not b.use_paint_weight:
continue
b.use_frontface = self.front_faces
def update_accumulate(self, context):
for b in bpy.data.brushes:
if not b.use_paint_weight:
continue
b.use_accumulate = self.accumulate
def update_falloff_shape(self, context):
for b in bpy.data.brushes:
if not b.use_paint_weight:
continue
b.falloff_shape = self.falloff_shape
for i, val in enumerate(b.cursor_color_add):
if val > 0:
b.cursor_color_add[i] = (
0.5 if self.falloff_shape == 'SPHERE' else 2.0)
front_faces: BoolProperty(
name="Front Faces Only", description="Toggle the Front Faces Only setting for all weight brushes", update=update_front_faces)
accumulate: BoolProperty(
name="Accumulate", description="Toggle the Accumulate setting for all weight brushes", update=update_accumulate)
falloff_shape: EnumProperty(name="Falloff Type", description="Select the Falloff Shape setting for all weight brushes", update=update_falloff_shape,
items=[
('SPHERE', 'Sphere', "The brush influence falls off along a sphere whose center is the mesh under the cursor's pointer"),
('PROJECTED', 'Projected', "The brush influence falls off in a tube around the cursor. This is useful for painting backfaces, as long as Front Faces Only is off.")
]
)
# @classmethod
# def poll(cls, context):
# return context.mode == 'PAINT_WEIGHT'
def draw_operators(self, layout, context):
layout.label(text="Operators")
op = layout.operator(
'object.vertex_group_normalize_all',
text="Normalize Deform",
icon='IPO_SINE'
)
op.group_select_mode = 'BONE_DEFORM'
op.lock_active = False
row = layout.row()
row.operator("object.vertex_group_clean", icon='BRUSH_DATA',
text="Clean 0").group_select_mode = 'ALL'
row.operator(EASYWEIGHTS_OT_delete_empty_deform_groups.bl_idname,
text="Wipe Empty", icon='GROUP_BONE')
row.operator(EASYWEIGHTS_OT_delete_unused_vertex_groups.bl_idname,
text="Wipe Unused", icon='BRUSH_DATA')
def draw_minimal(self, layout, context):
overlay = context.space_data.overlay
row = layout.row(heading="Symmetry: ")
# Compatibility for versions between rB5502517c3c12086c111a and rBfa9b05149c2ca3915a4fb26.
if hasattr(context.weight_paint_object.data, "use_mirror_vertex_group_x"):
row.prop(context.weight_paint_object.data,
"use_mirror_vertex_group_x", text="X-Mirror", toggle=True)
else:
row.prop(context.weight_paint_object.data,
"use_mirror_x", text="X-Mirror", toggle=True)
if hasattr(context.weight_paint_object.data, 'use_mirror_vertex_groups'):
row.prop(context.weight_paint_object.data,
'use_mirror_vertex_groups', text="Flip Groups", toggle=True)
row = layout.row(heading="Mesh Display: ")
row.prop(overlay, "show_wpaint_contours",
text="Weight Contours", toggle=True)
row.prop(overlay, "show_paint_wire", text="Wireframe", toggle=True)
row = layout.row(heading="Bone Display: ")
row.prop(overlay, "show_bones", text="Bones", toggle=True)
if context.pose_object:
row.prop(context.pose_object, "show_in_front", toggle=True)
self.draw_operators(layout, context)
def draw_overlay_settings(self, layout, context):
overlay = context.space_data.overlay
tool_settings = context.tool_settings
layout.label(text="Overlay")
row = layout.row()
row.use_property_split = True
row.prop(tool_settings, "vertex_group_user",
text="Zero Weights Display", expand=True)
if hasattr(context.space_data, "overlay"):
row = layout.row()
row.prop(overlay, "show_wpaint_contours",
text="Weight Contours", toggle=True)
row.prop(overlay, "show_paint_wire", text="Wireframe", toggle=True)
row.prop(overlay, "show_bones", text="Bones", toggle=True)
if context.pose_object:
layout.label(text="Armature Display")
layout.prop(context.pose_object.data, "display_type", expand=True)
layout.prop(context.pose_object, "show_in_front", toggle=True)
def draw_weight_paint_settings(self, layout, context):
tool_settings = context.tool_settings
layout.label(text="Weight Paint settings")
prefs = get_addon_prefs(context)
row = layout.row()
row.prop(tool_settings, "use_auto_normalize",
text="Auto Normalize", toggle=True)
row.prop(prefs, "auto_clean_weights", toggle=True)
row.prop(tool_settings, "use_multipaint",
text="Multi-Paint", toggle=True)
row = layout.row()
# Compatibility for versions between rB5502517c3c12086c111a and rBfa9b05149c2ca3915a4fb26.
if hasattr(context.weight_paint_object.data, "use_mirror_vertex_group_x"):
row.prop(context.weight_paint_object.data,
"use_mirror_vertex_group_x", text="X-Mirror", toggle=True)
else:
row.prop(context.weight_paint_object.data,
"use_mirror_x", text="X-Mirror", toggle=True)
if hasattr(context.weight_paint_object.data, 'use_mirror_vertex_groups'):
row.prop(context.weight_paint_object.data,
'use_mirror_vertex_groups', text="Flip Groups", toggle=True)
def draw_brush_settings(self, layout, context):
row = layout.row()
row.label(text="Brush Settings (Global)")
icon = 'HIDE_ON' if context.scene.easyweight_minimal else 'HIDE_OFF'
row.prop(context.scene, "easyweight_minimal",
icon=icon, toggle=False, text="", emboss=False)
layout.prop(self, "accumulate", toggle=True)
layout.prop(self, "front_faces", toggle=True)
row = layout.row(heading="Falloff Shape: ")
row.prop(self, "falloff_shape", expand=True)
layout.separator()
def draw(self, context):
layout = self.layout
self.draw_brush_settings(layout, context)
layout.separator()
if context.scene.easyweight_minimal:
self.draw_minimal(layout, context)
return
self.draw_weight_paint_settings(layout, context)
layout.separator()
self.draw_overlay_settings(layout, context)
layout.separator()
self.draw_operators(layout, context)
def invoke(self, context, event):
active_brush = context.tool_settings.weight_paint.brush
self.front_faces = active_brush.use_frontface
self.falloff_shape = active_brush.falloff_shape
self.accumulate = active_brush.use_accumulate
if 'clean_weights' not in context.scene:
context.scene['clean_weights'] = False
self.clean_weights = context.scene['clean_weights']
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
return {'FINISHED'}
def draw_menu_entry(self, context):
self.layout.operator(EASYWEIGHT_OT_wp_context_menu.bl_idname)
registry = [
EASYWEIGHT_OT_wp_context_menu
]
def register():
bpy.types.Scene.easyweight_minimal = BoolProperty(
name="Minimal",
description="Hide options that are less frequently used",
default=False
)
bpy.types.VIEW3D_MT_paint_weight.append(draw_menu_entry)
def unregister():
del bpy.types.Scene.easyweight_minimal
bpy.types.VIEW3D_MT_paint_weight.remove(draw_menu_entry)

View File

@ -0,0 +1,124 @@
from bpy.types import Menu
from .prefs import get_addon_prefs
from .vertex_group_operators import get_deforming_armature
class EASYWEIGHT_MT_PIE_easy_weight(Menu):
bl_label = "Easy Weight"
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
prefs = get_addon_prefs(context)
# 1) < Operators
self.draw_operators(pie.column().box(), context)
# 2) > Front Faces Only
pie.prop(
prefs,
'global_front_faces_only',
icon='OVERLAY',
text="Paint Through Mesh",
invert_checkbox=True,
)
# 3) V Overlay & Armature Display settings
self.draw_overlay_settings(pie.column().box(), context)
# 4) ^ Accumulate
pie.prop(prefs, 'global_accumulate', icon='GP_SELECT_STROKES')
# 5) <^ Empty
pie.separator()
# 6) ^> Toggle Falloff Shape
icon = 'SPHERE' if prefs.global_falloff_shape_sphere else 'MESH_CIRCLE'
text = "Spherical" if prefs.global_falloff_shape_sphere else "Projected Circle"
pie.prop(
prefs,
'global_falloff_shape_sphere',
text="Falloff Shape: " + text,
icon=icon,
invert_checkbox=prefs.global_falloff_shape_sphere,
)
# 7) <v Empty
pie.separator()
# 8) v>Empty
pie.separator()
def draw_operators(self, layout, context):
layout.label(text="Operators")
prefs = get_addon_prefs(context)
deform_rig = get_deforming_armature(context.active_object)
if deform_rig:
layout.operator('object.focus_deform_vgroups', icon='ZOOM_IN')
op = layout.operator(
'object.vertex_group_normalize_all', text="Normalize Deform Groups", icon='IPO_SINE'
)
op.group_select_mode = 'BONE_DEFORM'
op.lock_active = False
layout.operator(
'object.delete_empty_deform_vgroups',
text="Clear Empty Deform Groups",
icon='GROUP_BONE',
)
if not prefs.auto_clean_weights:
layout.operator(
"object.vertex_group_clean", icon='BRUSH_DATA', text="Clean Zero-Weights"
).group_select_mode = 'ALL'
layout.operator(
'object.delete_unused_vgroups', text="Clear Unused Groups", icon='BRUSH_DATA'
)
layout.operator(
'paint.weight_from_bones', text="Assign Automatic from Bones", icon='BONE_DATA'
).type = 'AUTOMATIC'
op = layout.operator(
'object.vertex_group_normalize_all', text="Normalize Deform", icon='IPO_SINE'
)
op.group_select_mode = 'BONE_DEFORM'
op.lock_active = False
def draw_overlay_settings(self, layout, context):
overlay = context.space_data.overlay
tool_settings = context.tool_settings
prefs = get_addon_prefs(context)
layout.label(text="Overlay")
if not prefs.always_show_zero_weights:
row = layout.row()
row.prop(tool_settings, "vertex_group_user", text="Zero Weights Display", expand=True)
if hasattr(context.space_data, "overlay"):
row = layout.row()
row.prop(
overlay,
"show_wpaint_contours",
text="Weight Contours",
toggle=True,
icon='MOD_INSTANCE',
)
row.prop(overlay, "show_paint_wire", text="Wireframe", toggle=True, icon='SHADING_WIRE')
icon = 'HIDE_OFF' if overlay.show_bones else 'HIDE_ON'
row.prop(overlay, "show_bones", text="Bones", toggle=True, icon=icon)
if context.pose_object:
col = layout.column()
col.label(text="Armature Display")
row = col.row(align=True)
row.prop(context.pose_object.data, "display_type", expand=True)
x_row = col.row()
x_row.prop(context.pose_object, "show_in_front", toggle=True, icon='XRAY')
if overlay.show_xray_bone:
x_row.prop(overlay, 'show_xray_bone', text="X-Ray Overlay")
registry = [
EASYWEIGHT_MT_PIE_easy_weight,
]