2025-12-01
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
from .weight_cleaner import start_cleaner, stop_cleaner
|
||||
from .utils import get_addon_prefs
|
||||
from .prefs_to_disk import PrefsFileSaveLoadMixin, update_prefs_on_file
|
||||
from pathlib import Path
|
||||
|
||||
def ensure_brush_assets():
|
||||
# Since the Brush Assets in Blender 4.3, brushes are not local to the .blend file
|
||||
# until they are first accessed, so let's do that when needed. We also can't check
|
||||
# whether these brushes exist without looping over all of them.
|
||||
for brush_name in 'Blur', 'Paint':
|
||||
for brush in bpy.data.brushes:
|
||||
if not brush.use_paint_weight:
|
||||
continue
|
||||
else:
|
||||
# Link the brush from the `datafiles` folder.
|
||||
blend_path = (Path(bpy.utils.resource_path('LOCAL')) / "datafiles/assets/brushes/essentials_brushes-mesh_weight.blend").as_posix()
|
||||
with bpy.data.libraries.load(blend_path, link=True) as (data_from, data_to):
|
||||
data_to.brushes = [brush_name]
|
||||
if brush_name == 'Paint':
|
||||
brush = bpy.data.brushes.get(('Paint', blend_path))
|
||||
brush.blend = 'ADD'
|
||||
|
||||
def get_available_wp_brushes():
|
||||
for brush in bpy.data.brushes:
|
||||
if brush.use_paint_weight:
|
||||
yield brush
|
||||
|
||||
|
||||
class EASYWEIGHT_addon_preferences(PrefsFileSaveLoadMixin, bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
always_show_zero_weights: BoolProperty(
|
||||
name="Always Show Zero Weights",
|
||||
description="A lack of weights will always be indicated with black color to differentiate it from a weight of 0.0 being assigned",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
always_auto_normalize: BoolProperty(
|
||||
name="Always Auto Normalize",
|
||||
description="Weight auto-normalization will always be turned on, so the sum of all deforming weights on a vertex always add up to 1",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
always_multipaint: BoolProperty(
|
||||
name="Always Multi-Paint",
|
||||
description="Multi-paint will always be turned on, allowing you to select more than one deforming bone while weight painting",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
always_xray: BoolProperty(
|
||||
name="Always X-Ray",
|
||||
description="Always enable bone x-ray when entering weight paint mode",
|
||||
default=True,
|
||||
update=update_prefs_on_file,
|
||||
)
|
||||
|
||||
def update_auto_clean(self, context):
|
||||
update_prefs_on_file()
|
||||
if self.auto_clean_weights:
|
||||
start_cleaner()
|
||||
else:
|
||||
stop_cleaner()
|
||||
|
||||
auto_clean_weights: BoolProperty(
|
||||
name="Always Auto Clean",
|
||||
description="While this is enabled, zero-weights will be removed automatically after every brush stroke",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def update_front_faces(self, context):
|
||||
update_prefs_on_file()
|
||||
for brush in get_available_wp_brushes():
|
||||
brush.use_frontface = self.global_front_faces_only
|
||||
|
||||
def update_accumulate(self, context):
|
||||
update_prefs_on_file()
|
||||
for brush in get_available_wp_brushes():
|
||||
brush.use_accumulate = self.global_accumulate
|
||||
|
||||
def update_falloff_shape(self, context):
|
||||
update_prefs_on_file()
|
||||
for brush in get_available_wp_brushes():
|
||||
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",
|
||||
default=False,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, 'auto_clean_weights')
|
||||
col.prop(self, 'always_show_zero_weights')
|
||||
col.prop(self, 'always_auto_normalize')
|
||||
col.prop(self, 'always_multipaint')
|
||||
col.prop(self, 'always_xray')
|
||||
|
||||
main_col = layout.column(align=True)
|
||||
hotkey_col = self.draw_fake_dropdown(main_col, self, 'show_hotkeys', "Hotkeys")
|
||||
if self.show_hotkeys:
|
||||
type(self).draw_hotkey_list(hotkey_col, context)
|
||||
|
||||
# NOTE: This function is copied from CloudRig's prefs.py. TODO: No longer needed since like 4.2 or so, could just use layout.panel(), but then bump the minimum blender version.
|
||||
def draw_fake_dropdown(self, layout, prop_owner, prop_name, dropdown_text):
|
||||
row = layout.row()
|
||||
split = row.split(factor=0.20)
|
||||
split.use_property_split = False
|
||||
prop_value = prop_owner.path_resolve(prop_name)
|
||||
icon = 'TRIA_DOWN' if prop_value else 'TRIA_RIGHT'
|
||||
split.prop(prop_owner, prop_name, icon=icon, emboss=False, text=dropdown_text)
|
||||
split.prop(prop_owner, prop_name, icon='BLANK1', emboss=False, text="")
|
||||
split = layout.split(factor=0.012)
|
||||
split.row()
|
||||
dropdown_row = split.row()
|
||||
dropdown_col = dropdown_row.column()
|
||||
row = dropdown_col.row()
|
||||
row.use_property_split = False
|
||||
|
||||
return dropdown_col
|
||||
|
||||
@classmethod
|
||||
def draw_hotkey_list(cls, layout, context):
|
||||
hotkey_class = cls
|
||||
user_kc = context.window_manager.keyconfigs.user
|
||||
|
||||
global EASYWEIGHT_KEYMAPS
|
||||
|
||||
prev_kmi = None
|
||||
for addon_km, addon_kmi in EASYWEIGHT_KEYMAPS:
|
||||
user_km = user_kc.keymaps.get(addon_km.name)
|
||||
if not user_km:
|
||||
# This really shouldn't happen.
|
||||
continue
|
||||
for user_kmi in user_km.keymap_items:
|
||||
if user_kmi.idname != addon_kmi.idname:
|
||||
continue
|
||||
if user_kmi.idname == 'wm.call_menu_pie' and user_kmi.properties.name != addon_kmi.properties.name:
|
||||
continue
|
||||
col = layout.column()
|
||||
col.context_pointer_set("keymap", user_km)
|
||||
if user_kmi and prev_kmi and prev_kmi.name != user_kmi.name:
|
||||
col.separator()
|
||||
user_row = col.row()
|
||||
|
||||
hotkey_class.draw_kmi(user_km, user_kmi, user_row)
|
||||
break
|
||||
|
||||
# NOTE: This function is copied from CloudRig's cloudrig.py.
|
||||
@staticmethod
|
||||
def draw_kmi(km, kmi, layout):
|
||||
"""A simplified version of draw_kmi from rna_keymap_ui.py."""
|
||||
col = layout.column()
|
||||
|
||||
split = col.split(factor=0.7)
|
||||
|
||||
# header bar
|
||||
row = split.row(align=True)
|
||||
row.prop(kmi, "active", text="", emboss=False)
|
||||
row.label(text=f'{kmi.name} ({km.name})')
|
||||
|
||||
row = split.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.enabled = kmi.active
|
||||
sub.prop(kmi, "type", text="", full_event=True)
|
||||
|
||||
if kmi.is_user_modified:
|
||||
row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id
|
||||
|
||||
# NOTE: This function is copied from CloudRig's cloudrig.py.
|
||||
@staticmethod
|
||||
def find_kmi_in_km_by_hash(keymap, kmi_hash):
|
||||
"""There's no solid way to match modified user keymap items to their
|
||||
add-on equivalent, which is necessary to draw them in the UI reliably.
|
||||
|
||||
To remedy this, we store a hash in the KeyMapItem's properties.
|
||||
|
||||
This function lets us find a KeyMapItem with a stored hash in a KeyMap.
|
||||
Eg., we can pass a User KeyMap and an Addon KeyMapItem's hash, to find the
|
||||
corresponding user keymap, even if it was modified.
|
||||
|
||||
The hash value is unfortunately exposed to the users, so we just hope they don't touch that.
|
||||
"""
|
||||
|
||||
for kmi in keymap.keymap_items:
|
||||
if not kmi.properties:
|
||||
continue
|
||||
if 'hash' not in kmi.properties:
|
||||
continue
|
||||
|
||||
if kmi.properties['hash'] == kmi_hash:
|
||||
return kmi
|
||||
|
||||
EASYWEIGHT_KEYMAPS = []
|
||||
|
||||
@persistent
|
||||
def set_brush_prefs_on_file_load(scene):
|
||||
if bpy.app.version >= (4, 3, 0):
|
||||
ensure_brush_assets()
|
||||
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
|
||||
|
||||
|
||||
def register_hotkey(
|
||||
bl_idname, hotkey_kwargs, *, key_cat='Window', space_type='EMPTY', op_kwargs={}
|
||||
):
|
||||
"""This function inserts a 'hash' into the created KeyMapItems' properties,
|
||||
so they can be compared to each other, and duplicates can be avoided."""
|
||||
|
||||
wm = bpy.context.window_manager
|
||||
addon_keyconfig = wm.keyconfigs.addon
|
||||
if not addon_keyconfig:
|
||||
# This happens when running Blender in background mode.
|
||||
return
|
||||
|
||||
addon_keymaps = addon_keyconfig.keymaps
|
||||
addon_km = addon_keymaps.get(key_cat)
|
||||
if not addon_km:
|
||||
addon_km = addon_keymaps.new(name=key_cat, space_type=space_type)
|
||||
|
||||
addon_kmi = addon_km.keymap_items.new(bl_idname, **hotkey_kwargs)
|
||||
for key in op_kwargs:
|
||||
value = op_kwargs[key]
|
||||
setattr(addon_kmi.properties, key, value)
|
||||
|
||||
global EASYWEIGHT_KEYMAPS
|
||||
EASYWEIGHT_KEYMAPS.append((addon_km, addon_kmi))
|
||||
|
||||
|
||||
registry = [EASYWEIGHT_addon_preferences]
|
||||
|
||||
|
||||
def register():
|
||||
register_hotkey(
|
||||
'wm.call_menu_pie',
|
||||
hotkey_kwargs={'type': "W", 'value': "PRESS"},
|
||||
key_cat='Weight Paint',
|
||||
op_kwargs={'name': 'EASYWEIGHT_MT_PIE_easy_weight'},
|
||||
)
|
||||
bpy.app.handlers.load_post.append(set_brush_prefs_on_file_load)
|
||||
EASYWEIGHT_addon_preferences.register_autoload_from_file()
|
||||
|
||||
|
||||
def unregister_hotkeys():
|
||||
for km, kmi in EASYWEIGHT_KEYMAPS:
|
||||
km.keymap_items.remove(kmi)
|
||||
|
||||
|
||||
def unregister():
|
||||
unregister_hotkeys()
|
||||
bpy.app.handlers.load_post.remove(set_brush_prefs_on_file_load)
|
||||
Reference in New Issue
Block a user