2025-12-01
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import GizmoGroup, Gizmo, PoseBone, Operator
|
||||
from .utils import get_addon_prefs
|
||||
|
||||
### MSGBUS FUNCTIONS ###
|
||||
# The Gizmo API doesn't provide the necessary callbacks to do careful partial
|
||||
# updates of gizmo data based on what properties are modified.
|
||||
# However, the msgbus system allows us to create our own connections between
|
||||
# Blender properties and the functions that should be called when those properties
|
||||
# change.
|
||||
|
||||
# msgbus system needs an arbitrary python object as storage, so here it is.
|
||||
gizmo_msgbus = object()
|
||||
|
||||
def mb_ensure_gizmos_on_active_armature(gizmo_group):
|
||||
"""Ensure gizmos exist for all PoseBones that need them."""
|
||||
context = bpy.context
|
||||
obj = context.object
|
||||
|
||||
for pose_bone in obj.pose.bones:
|
||||
try:
|
||||
if pose_bone.enable_bone_gizmo and pose_bone.name not in gizmo_group.widgets:
|
||||
gizmo = gizmo_group.create_gizmo(context, pose_bone)
|
||||
elif not pose_bone.bone_gizmo.shape_object:
|
||||
for bone_name, gizmo in gizmo_group.widgets.items():
|
||||
if bone_name == pose_bone.name:
|
||||
gizmo.custom_shape = None
|
||||
|
||||
except ReferenceError:
|
||||
# StructRNA of type BoneGizmoGroup has been removed.
|
||||
# TODO: Not sure when this happens.
|
||||
pass
|
||||
|
||||
def mb_refresh_all_gizmo_colors(gizmo_group):
|
||||
"""Keep Gizmo colors in sync with addon preferences."""
|
||||
context = bpy.context
|
||||
|
||||
try:
|
||||
for bone_name, gizmo in gizmo_group.widgets.items():
|
||||
gizmo.refresh_colors(context)
|
||||
except ReferenceError:
|
||||
# StructRNA of type BoneGizmoGroup has been removed.
|
||||
# TODO: Not sure when this happens.
|
||||
pass
|
||||
|
||||
def mb_refresh_single_gizmo(gizmo_group, bone_name):
|
||||
"""Refresh Gizmo behaviour settings. This should be called when the user changes
|
||||
the Gizmo settings in the Properties editor.
|
||||
"""
|
||||
if not gizmo_group:
|
||||
return
|
||||
context = bpy.context
|
||||
pose_bone = context.object.pose.bones.get(bone_name)
|
||||
gizmo_props = pose_bone.bone_gizmo
|
||||
try:
|
||||
gizmo = gizmo_group.widgets[bone_name]
|
||||
except:
|
||||
return
|
||||
|
||||
if gizmo_props.operator != 'None':
|
||||
op_name = gizmo_props.operator
|
||||
if op_name == 'transform.rotate' and gizmo_props.rotation_mode == 'TRACKBALL':
|
||||
op_name = 'transform.trackball'
|
||||
|
||||
op = gizmo.target_set_operator(op_name)
|
||||
|
||||
if op_name == 'transform.rotate' and gizmo_props.rotation_mode in 'XYZ':
|
||||
op.orient_type = 'LOCAL'
|
||||
op.constraint_axis = [axis == gizmo_props.rotation_mode for axis in 'XYZ']
|
||||
|
||||
if op_name in ['transform.translate', 'transform.resize']:
|
||||
op.constraint_axis = gizmo_props.transform_axes
|
||||
|
||||
gizmo.init_properties(context)
|
||||
|
||||
def mb_refresh_single_gizmo_shape(gizmo_group, bone_name):
|
||||
"""Re-calculate a gizmo's vertex indicies. This is expensive, so it should
|
||||
be called sparingly."""
|
||||
context = bpy.context
|
||||
print("refresh single gizmo shape")
|
||||
if not context.object or context.object.type != 'ARMATURE' or context.object.mode != 'POSE':
|
||||
return
|
||||
try:
|
||||
gizmo = gizmo_group.widgets[bone_name]
|
||||
gizmo.init_shape(context)
|
||||
except:
|
||||
pass
|
||||
|
||||
class BoneGizmoGroup(GizmoGroup):
|
||||
"""This single GizmoGroup manages all bone gizmos for all rigs.""" # TODO: Currently this will have issues when there are two rigs with similar bone names. Rig object names should be included when identifying widgets.
|
||||
bl_idname = "OBJECT_GGT_bone_gizmo"
|
||||
bl_label = "Bone Gizmos"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_options = {
|
||||
'3D' # Lets Gizmos use the 'draw_select' function to draw into a selection pass.
|
||||
,'PERSISTENT'
|
||||
,'SHOW_MODAL_ALL' # TODO what is this
|
||||
,'DEPTH_3D' # Provides occlusion but results in Z-fighting when gizmo geometry isn't offset from the source mesh.
|
||||
,'SELECT' # I thought this would make Gizmo.select do something but doesn't seem that way
|
||||
,'SCALE' # This makes all gizmos' scale relative to the world rather than the camera, so we don't need to set use_draw_scale on each Gizmo. (And that option does nothing because of this one)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.scene.bone_gizmos_enabled and context.object \
|
||||
and context.object.type == 'ARMATURE' and context.object.mode=='POSE'
|
||||
|
||||
def setup(self, context):
|
||||
"""Runs when this GizmoGroup is created, I think.
|
||||
Executed by Blender on launch, and for some reason
|
||||
also when changing bone group colors. WHAT!?"""
|
||||
self.widgets = {}
|
||||
mb_ensure_gizmos_on_active_armature(self)
|
||||
|
||||
# Hook up the addon preferences to color refresh function
|
||||
# using msgbus system.
|
||||
addon_prefs = get_addon_prefs(context)
|
||||
global gizmo_msgbus
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key = addon_prefs.path_resolve('preferences', False)
|
||||
,owner = gizmo_msgbus
|
||||
,args = (self,)
|
||||
,notify = mb_refresh_all_gizmo_colors
|
||||
)
|
||||
|
||||
# Hook up Custom Gizmo checkbox to a function that will ensure that
|
||||
# a Gizmo instance actually exists for each bone that needs one.
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key = (PoseBone, "enable_bone_gizmo")
|
||||
,owner = gizmo_msgbus
|
||||
,args = (self,)
|
||||
,notify = mb_ensure_gizmos_on_active_armature
|
||||
)
|
||||
|
||||
def create_gizmo(self, context, pose_bone) -> Gizmo:
|
||||
"""Add a gizmo to this GizmoGroup based on user-defined properties."""
|
||||
gizmo_props = pose_bone.bone_gizmo
|
||||
|
||||
if not pose_bone.enable_bone_gizmo:
|
||||
return
|
||||
|
||||
gizmo = self.gizmos.new('GIZMO_GT_bone_gizmo')
|
||||
gizmo.bone_name = pose_bone.name
|
||||
|
||||
# Hook up gizmo properties (the ones that can be customized by user)
|
||||
# to the gizmo refresh functions, using msgbus system.
|
||||
global gizmo_msgbus
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key = gizmo_props
|
||||
,owner = gizmo_msgbus
|
||||
,args = (self, gizmo.bone_name)
|
||||
,notify = mb_refresh_single_gizmo
|
||||
)
|
||||
|
||||
for prop_name in ['shape_object', 'vertex_group_name', 'face_map_name', 'use_face_map']:
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key = gizmo_props.path_resolve(prop_name, False)
|
||||
,owner = gizmo_msgbus
|
||||
,args = (self, gizmo.bone_name)
|
||||
,notify = mb_refresh_single_gizmo_shape
|
||||
)
|
||||
|
||||
self.widgets[pose_bone.name] = gizmo
|
||||
mb_refresh_single_gizmo(self, pose_bone.name)
|
||||
mb_refresh_single_gizmo_shape(self, pose_bone.name)
|
||||
|
||||
return gizmo
|
||||
|
||||
def refresh(self, context):
|
||||
"""This is a Gizmo API function, called by Blender on what seems to be
|
||||
depsgraph updates and frame changes.
|
||||
Refresh all visible gizmos that use vertex group masking.
|
||||
This should be done whenever a bone position changes.
|
||||
This should be kept performant!
|
||||
"""
|
||||
dg = context.evaluated_depsgraph_get()
|
||||
eval_meshes = {}
|
||||
|
||||
for bonename, gizmo in self.widgets.items():
|
||||
pb = gizmo.get_pose_bone(context)
|
||||
|
||||
if not gizmo or not gizmo.is_using_vgroup(context) or not gizmo.poll(context):
|
||||
continue
|
||||
|
||||
obj = pb.bone_gizmo.shape_object
|
||||
if obj.name in eval_meshes:
|
||||
eval_mesh = eval_meshes[obj.name]
|
||||
else:
|
||||
eval_meshes[obj.name] = eval_mesh = obj.evaluated_get(dg).to_mesh()
|
||||
eval_mesh.calc_loop_triangles()
|
||||
try:
|
||||
gizmo.refresh_shape_vgroup(context, eval_mesh)
|
||||
except ReferenceError:
|
||||
# For some reason sometimes it complains that StructRNA of the Mesh has been removed. I don't get why.
|
||||
pass
|
||||
|
||||
class BONEGIZMO_OT_RestartGizmoGroup(Operator):
|
||||
"""Re-initialize all gizmos. Needed when the gizmo shape objects are modified, since there's no dependency between gizmos and their target shapes"""
|
||||
|
||||
bl_idname = "pose.restart_gizmos"
|
||||
bl_label = "Refresh Gizmos"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.utils.unregister_class(BoneGizmoGroup)
|
||||
bpy.utils.register_class(BoneGizmoGroup)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
registry = [
|
||||
BoneGizmoGroup,
|
||||
BONEGIZMO_OT_RestartGizmoGroup
|
||||
]
|
||||
|
||||
def unregister():
|
||||
# Unhook everything from msgbus system
|
||||
global gizmo_msgbus
|
||||
bpy.msgbus.clear_by_owner(gizmo_msgbus)
|
||||
Reference in New Issue
Block a user