Files
blender-portable-repo/scripts/addons/easy_weight/prefs.py
T
2026-03-17 14:58:51 -06:00

285 lines
10 KiB
Python

# 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)