Files
test2/scripts/startup/bl_ui/properties_data_bone.py
Christoph Lendenfeld f9ea64b0ba Anim: Per bone wire width for custom shapes
The setting adds the "Custom Shape Wire Width"
option to the "Viewport Display/Custom Shape" section of a pose bone.
As the setting says, this controls how thick the wire is drawn in the viewport.

This is done by adding a geometry shader that makes two triangles out of a line.

The Anti-Aliasing is controlled by the setting
Viewport->Quality->Smooth Wires->Overlay in the user preferences.

## Artifacts
When increasing the line width, the lines start to separate at their vertices.
This comes from extruding each edge along the normal of its direction.
This could be solved by adding round caps in a later PR.

Pull Request: https://projects.blender.org/blender/blender/pulls/120176
2024-05-30 15:51:30 +02:00

617 lines
20 KiB
Python

# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Panel
import rna_prop_ui
from bpy.app.translations import contexts as i18n_contexts
class BoneButtonsPanel:
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "bone"
@classmethod
def poll(cls, context):
return (context.bone or context.edit_bone)
class BONE_PT_context_bone(BoneButtonsPanel, Panel):
bl_label = ""
bl_options = {'HIDE_HEADER'}
def draw(self, context):
layout = self.layout
bone = context.bone
if not bone:
bone = context.edit_bone
row = layout.row()
row.label(text="", icon='BONE_DATA')
row.prop(bone, "name", text="")
class BONE_PT_transform(BoneButtonsPanel, Panel):
bl_label = "Transform"
@classmethod
def poll(cls, context):
if context.edit_bone:
return True
ob = context.object
return ob and ob.mode == 'POSE' and context.bone
def draw(self, context):
layout = self.layout
layout.use_property_split = True
ob = context.object
bone = context.bone
col = layout.column()
if bone and ob:
pchan = ob.pose.bones[bone.name]
col.active = not (bone.parent and bone.use_connect)
row = col.row(align=True)
row.prop(pchan, "location")
row.use_property_decorate = False
row.prop(pchan, "lock_location", text="", emboss=False, icon='DECORATE_UNLOCKED')
rotation_mode = pchan.rotation_mode
if rotation_mode == 'QUATERNION':
col = layout.column()
row = col.row(align=True)
row.prop(pchan, "rotation_quaternion", text="Rotation")
sub = row.column(align=True)
sub.use_property_decorate = False
sub.prop(pchan, "lock_rotation_w", text="", emboss=False, icon='DECORATE_UNLOCKED')
sub.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
elif rotation_mode == 'AXIS_ANGLE':
col = layout.column()
row = col.row(align=True)
row.prop(pchan, "rotation_axis_angle", text="Rotation")
sub = row.column(align=True)
sub.use_property_decorate = False
sub.prop(pchan, "lock_rotation_w", text="", emboss=False, icon='DECORATE_UNLOCKED')
sub.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
else:
col = layout.column()
row = col.row(align=True)
row.prop(pchan, "rotation_euler", text="Rotation")
row.use_property_decorate = False
row.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
row = layout.row(align=True)
row.prop(pchan, "rotation_mode", text="Mode")
row.label(text="", icon='BLANK1')
col = layout.column()
row = col.row(align=True)
row.prop(pchan, "scale")
row.use_property_decorate = False
row.prop(pchan, "lock_scale", text="", emboss=False, icon='DECORATE_UNLOCKED')
elif context.edit_bone:
bone = context.edit_bone
col = layout.column()
col.prop(bone, "head")
col.prop(bone, "tail")
col = layout.column()
col.prop(bone, "roll")
col.prop(bone, "length")
col.prop(bone, "lock")
class BONE_PT_curved(BoneButtonsPanel, Panel):
bl_label = "Bendy Bones"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
ob = context.object
bone = context.bone
arm = context.armature
bone_list = "bones"
if ob and bone:
bbone = ob.pose.bones[bone.name]
elif bone is None:
bone = context.edit_bone
bbone = bone
bone_list = "edit_bones"
else:
bbone = bone
layout = self.layout
layout.use_property_split = True
layout.prop(bone, "bbone_segments", text="Segments")
col = layout.column(align=True)
col.prop(bone, "bbone_x", text="Display Size X")
col.prop(bone, "bbone_z", text="Z")
topcol = layout.column()
topcol.active = bone.bbone_segments > 1
topcol.prop(bone, "bbone_mapping_mode", text="Vertex Mapping")
col = topcol.column(align=True)
col.prop(bbone, "bbone_curveinx", text="Curve In X")
col.prop(bbone, "bbone_curveinz", text="Z")
col = topcol.column(align=True)
col.prop(bbone, "bbone_curveoutx", text="Curve Out X")
col.prop(bbone, "bbone_curveoutz", text="Z")
col = topcol.column(align=True)
col.prop(bbone, "bbone_rollin", text="Roll In")
col.prop(bbone, "bbone_rollout", text="Out", text_ctxt=i18n_contexts.id_armature)
col.prop(bone, "use_endroll_as_inroll")
col = topcol.column(align=True)
col.prop(bbone, "bbone_scalein", text="Scale In")
col = topcol.column(align=True)
col.prop(bbone, "bbone_scaleout", text="Scale Out")
col = topcol.column(align=True)
col.prop(bbone, "bbone_easein", text="Ease In", text_ctxt=i18n_contexts.id_armature)
col.prop(bbone, "bbone_easeout", text="Out", text_ctxt=i18n_contexts.id_armature)
col.prop(bone, "use_scale_easing")
col = topcol.column(align=True)
col.prop(bone, "bbone_handle_type_start", text="Start Handle")
col2 = col.column(align=True)
col2.active = (bone.bbone_handle_type_start != 'AUTO')
col2.prop_search(bone, "bbone_custom_handle_start", arm, bone_list, text="Custom")
row = col.row(align=True)
row.use_property_split = False
split = row.split(factor=0.4)
split.alignment = 'RIGHT'
split.label(text="Scale")
split2 = split.split(factor=0.7)
row2 = split2.row(align=True)
row2.prop(bone, "bbone_handle_use_scale_start", index=0, text="X", toggle=True)
row2.prop(bone, "bbone_handle_use_scale_start", index=1, text="Y", toggle=True)
row2.prop(bone, "bbone_handle_use_scale_start", index=2, text="Z", toggle=True)
split2.prop(bone, "bbone_handle_use_ease_start", text="Ease", text_ctxt=i18n_contexts.id_armature, toggle=True)
row.label(icon='BLANK1')
col = topcol.column(align=True)
col.prop(bone, "bbone_handle_type_end", text="End Handle")
col2 = col.column(align=True)
col2.active = (bone.bbone_handle_type_end != 'AUTO')
col2.prop_search(bone, "bbone_custom_handle_end", arm, bone_list, text="Custom")
row = col.row(align=True)
row.use_property_split = False
split = row.split(factor=0.4)
split.alignment = 'RIGHT'
split.label(text="Scale")
split2 = split.split(factor=0.7)
row2 = split2.row(align=True)
row2.prop(bone, "bbone_handle_use_scale_end", index=0, text="X", toggle=True)
row2.prop(bone, "bbone_handle_use_scale_end", index=1, text="Y", toggle=True)
row2.prop(bone, "bbone_handle_use_scale_end", index=2, text="Z", toggle=True)
split2.prop(bone, "bbone_handle_use_ease_end", text="Ease", text_ctxt=i18n_contexts.id_armature, toggle=True)
row.label(icon='BLANK1')
class BONE_PT_relations(BoneButtonsPanel, Panel):
bl_options = {'DEFAULT_CLOSED'}
bl_label = "Relations"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
ob = context.object
bone = context.bone
arm = context.armature
pchan = None
if ob and bone:
pchan = ob.pose.bones[bone.name]
elif bone is None:
bone = context.edit_bone
col = layout.column()
if context.bone:
col.prop(bone, "parent")
else:
col.prop_search(bone, "parent", arm, "edit_bones")
if ob and pchan:
col.prop(bone, "use_relative_parent")
sub = col.column()
sub.active = (bone.parent is not None)
sub.prop(bone, "use_connect")
sub = col.column()
sub.active = (not bone.parent or not bone.use_connect)
sub.prop(bone, "use_local_location")
sub = col.column()
sub.active = (bone.parent is not None)
sub.prop(bone, "use_inherit_rotation")
sub.prop(bone, "inherit_scale")
class BONE_PT_collections(BoneButtonsPanel, Panel):
bl_label = "Bone Collections"
bl_parent_id = "BONE_PT_relations"
@classmethod
def poll(cls, context):
return context.bone or context.edit_bone
def draw(self, context):
layout = self.layout
layout.use_property_split = False
bone = context.bone or context.edit_bone
object = context.pose_object or context.edit_object or context.object
if not object:
layout.active = False
sub = layout.column(align=True)
sub.label(text="Cannot figure out which object this bone belongs to.")
sub.label(text="Please file a bug report.")
return
armature = object.data
is_solo_active = armature.collections.is_solo_active
if not bone.collections:
layout.active = False
layout.label(text="Not assigned to any bone collection.")
return
box = layout.box()
sub = box.column(align=True)
for bcoll in bone.collections:
bcoll_row = sub.row()
bcoll_row.emboss = 'NONE'
# Name & visibility of bcoll. Safe things, so aligned together.
row = bcoll_row.row(align=True)
row.label(text=bcoll.name)
# Sub-layout that's dimmed when the bone collection's own visibility flag doesn't matter.
sub_visible = row.row(align=True)
sub_visible.active = (not is_solo_active) and bcoll.is_visible_ancestors
sub_visible.prop(bcoll, "is_visible", text="", icon='HIDE_OFF' if bcoll.is_visible else 'HIDE_ON')
row.prop(bcoll, "is_solo", text="", icon='SOLO_ON' if bcoll.is_solo else 'SOLO_OFF')
# Unassignment operator, less safe so with a bit of spacing.
props = bcoll_row.operator("armature.collection_unassign_named", text="", icon='X')
props.name = bcoll.name
props.bone_name = bone.name
class BONE_PT_display(BoneButtonsPanel, Panel):
bl_label = "Viewport Display"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return context.bone or context.edit_bone
def draw(self, context):
# NOTE: this works ok in edit-mode but isn't
# all that useful so disabling for now.
layout = self.layout
layout.use_property_split = True
if context.bone is None:
self.draw_edit_bone(context, layout)
else:
self.draw_bone(context, layout)
def draw_bone(self, context, layout):
bone = context.bone
col = layout.column()
col.prop(bone, "hide", text="Hide", toggle=False)
# Figure out the pose bone.
ob = context.object
if not ob:
return
pose_bone = ob.pose.bones[bone.name]
# Allow the layout to use the space normally occupied by the 'set a key' diamond.
layout.use_property_decorate = False
row = layout.row(align=True)
row.prop(bone.color, "palette", text="Bone Color")
props = row.operator("armature.copy_bone_color_to_selected", text="", icon='UV_SYNC_SELECT')
props.bone_type = 'EDIT'
self.draw_bone_color_ui(layout, bone.color)
row = layout.row(align=True)
row.prop(pose_bone.color, "palette", text="Pose Bone Color")
props = row.operator("armature.copy_bone_color_to_selected", text="", icon='UV_SYNC_SELECT')
props.bone_type = 'POSE'
self.draw_bone_color_ui(layout, pose_bone.color)
def draw_edit_bone(self, context, layout):
bone = context.edit_bone
if bone is None:
return
col = layout.column()
col.prop(bone, "hide", text="Hide", toggle=False)
layout.prop(bone.color, "palette", text="Bone Color")
self.draw_bone_color_ui(layout, bone.color)
def draw_bone_color_ui(self, layout, bone_color):
if not bone_color.is_custom:
return
split = layout.split(factor=0.401)
col = split.column()
row = col.row()
row.alignment = 'RIGHT'
row.label(text="Custom Colors")
col = split.column(align=True)
row = col.row(align=True)
row.use_property_split = False
row.prop(bone_color.custom, "normal", text="")
row.prop(bone_color.custom, "select", text="")
row.prop(bone_color.custom, "active", text="")
class BONE_PT_display_custom_shape(BoneButtonsPanel, Panel):
bl_label = "Custom Shape"
bl_parent_id = "BONE_PT_display"
@classmethod
def poll(cls, context):
return context.bone
def draw(self, context):
layout = self.layout
layout.use_property_split = True
ob = context.object
bone = context.bone
pchan = None
if ob and bone:
pchan = ob.pose.bones[bone.name]
elif bone is None:
bone = context.edit_bone
if bone and pchan:
col = layout.column()
col.prop(pchan, "custom_shape")
sub = col.column()
sub.active = bool(pchan and pchan.custom_shape)
sub.separator()
sub.prop(pchan, "custom_shape_scale_xyz", text="Scale")
sub.prop(pchan, "custom_shape_translation", text="Translation")
sub.prop(pchan, "custom_shape_rotation_euler", text="Rotation")
sub.prop_search(pchan, "custom_shape_transform", ob.pose, "bones", text="Override Transform")
sub.prop(pchan, "use_custom_shape_bone_size")
sub.separator()
sub.prop(bone, "show_wire", text="Wireframe")
sub.prop(pchan, "custom_shape_wire_width")
class BONE_PT_inverse_kinematics(BoneButtonsPanel, Panel):
bl_label = "Inverse Kinematics"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
ob = context.object
return ob and ob.mode == 'POSE' and context.bone
def draw(self, context):
layout = self.layout
layout.use_property_split = True
ob = context.object
bone = context.bone
pchan = ob.pose.bones[bone.name]
active = pchan.is_in_ik_chain
col = layout.column()
col.prop(pchan, "ik_stretch", slider=True)
col.active = active
layout.separator()
col = layout.column(align=True)
col.prop(pchan, "lock_ik_x", text="Lock IK X")
col.prop(pchan, "lock_ik_y", text="Y")
col.prop(pchan, "lock_ik_z", text="Z")
col = layout.column(align=True)
sub = col.column(align=True)
sub.active = pchan.lock_ik_x is False and active
sub.prop(pchan, "ik_stiffness_x", text="Stiffness X", slider=True)
sub = col.column(align=True)
sub.active = pchan.lock_ik_y is False and active
sub.prop(pchan, "ik_stiffness_y", text="Y", slider=True)
sub = col.column(align=True)
sub.active = pchan.lock_ik_z is False and active
sub.prop(pchan, "ik_stiffness_z", text="Z", slider=True)
col = layout.column(align=True)
sub = col.column()
sub.active = pchan.lock_ik_x is False and active
sub.prop(pchan, "use_ik_limit_x", text="Limit X")
sub = col.column(align=True)
sub.active = pchan.lock_ik_x is False and pchan.use_ik_limit_x and active
sub.prop(pchan, "ik_min_x", text="Min")
sub.prop(pchan, "ik_max_x", text="Max")
col.separator()
sub = col.column()
sub.active = pchan.lock_ik_y is False and active
sub.prop(pchan, "use_ik_limit_y", text="Limit Y")
sub = col.column(align=True)
sub.active = pchan.lock_ik_y is False and pchan.use_ik_limit_y and active
sub.prop(pchan, "ik_min_y", text="Min")
sub.prop(pchan, "ik_max_y", text="Max")
col.separator()
sub = col.column()
sub.active = pchan.lock_ik_z is False and active
sub.prop(pchan, "use_ik_limit_z", text="Limit Z")
sub = col.column(align=True)
sub.active = pchan.lock_ik_z is False and pchan.use_ik_limit_z and active
sub.prop(pchan, "ik_min_z", text="Min")
sub.prop(pchan, "ik_max_z", text="Max")
col.separator()
if ob.pose.ik_solver == 'ITASC':
col = layout.column()
col.prop(pchan, "use_ik_rotation_control", text="Control Rotation")
col.active = active
col = layout.column()
col.prop(pchan, "ik_rotation_weight", text="IK Rotation Weight", slider=True)
col.active = active
# not supported yet
# row = layout.row()
# row.prop(pchan, "use_ik_linear_control", text="Joint Size")
# row.prop(pchan, "ik_linear_weight", text="Weight", slider=True)
class BONE_PT_deform(BoneButtonsPanel, Panel):
bl_label = "Deform"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, context):
bone = context.bone
if not bone:
bone = context.edit_bone
self.layout.prop(bone, "use_deform", text="")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
bone = context.bone
if not bone:
bone = context.edit_bone
layout.active = bone.use_deform
col = layout.column()
col.prop(bone, "envelope_distance", text="Envelope Distance")
col.prop(bone, "envelope_weight", text="Envelope Weight")
col.prop(bone, "use_envelope_multiply", text="Envelope Multiply")
col.separator()
col = layout.column(align=True)
col.prop(bone, "head_radius", text="Radius Head")
col.prop(bone, "tail_radius", text="Tail")
class BONE_PT_custom_props(BoneButtonsPanel, rna_prop_ui.PropertyPanel, Panel):
COMPAT_ENGINES = {
'BLENDER_RENDER',
'BLENDER_EEVEE',
'BLENDER_EEVEE_NEXT',
'BLENDER_WORKBENCH',
}
_property_type = bpy.types.Bone, bpy.types.EditBone, bpy.types.PoseBone
@classmethod
def _poll(cls, context):
context_path = cls._get_context_path(context)
rna_item, _context_member = rna_prop_ui.rna_idprop_context_value(context, context_path, cls._property_type)
return bool(rna_item)
def draw(self, context):
context_path = self._get_context_path(context)
rna_prop_ui.draw(self.layout, context, context_path, self._property_type)
@classmethod
def _get_context_path(self, context):
if context.mode == 'EDIT_ARMATURE':
# This also accounts for pinned armatures.
return "edit_bone"
obj = context.object
if not obj:
# We have to return _something_. If there is some bone by some
# miracle, just use it.
return "bone"
if obj.mode != 'POSE':
# Outside of pose mode, active_bone is the one to use. It's either a
# Bone or an EditBone, depending on the mode.
return "active_bone"
if context.active_pose_bone is not None:
# There is an active pose bone, so use it.
return "active_pose_bone"
# When the active bone is hidden, `context.active_pose_bone` is None, but
# `context.bone` still points to it. Use that to still get the pose bone.
if context.bone is None:
# If there is no active bone, let the rest of the code refer to the
# also-None active pose bone, as that's more appropriate given we're
# currently in pose mode.
return "active_pose_bone"
bone_path = obj.pose.bones[context.bone.name].path_from_id()
return "object." + bone_path
classes = (
BONE_PT_context_bone,
BONE_PT_transform,
BONE_PT_curved,
BONE_PT_relations,
BONE_PT_collections,
BONE_PT_inverse_kinematics,
BONE_PT_deform,
BONE_PT_display,
BONE_PT_display_custom_shape,
BONE_PT_custom_props,
)
if __name__ == "__main__": # only for live edit.
from bpy.utils import register_class
for cls in classes:
register_class(cls)