c65bed3ff4
- Added Highlight Bone System in the 3D View, can be turned off in settings. - Added more bones to the acceptable bone lists. - Fixed issue with properties registrations and unregistration, the system is more rebust now. - Added a validate t-pose system - Added a detect bone scales system. - Fixed some translation strings - Armature validation now uses logger system.
631 lines
20 KiB
Python
631 lines
20 KiB
Python
import bpy
|
|
from typing import List, Tuple, Optional, Any, Dict, Union, Callable
|
|
from bpy.types import PropertyGroup, Material, Scene, Object, Context
|
|
from bpy.props import (
|
|
StringProperty,
|
|
BoolProperty,
|
|
EnumProperty,
|
|
IntProperty,
|
|
FloatProperty,
|
|
CollectionProperty,
|
|
PointerProperty
|
|
)
|
|
from .logging_setup import logger
|
|
from .translations import t, get_languages_list, update_language
|
|
from .addon_preferences import get_preference, save_preference
|
|
from .updater import get_version_list
|
|
from .common import get_armature_list, get_active_armature, get_all_meshes, SceneMatClass
|
|
from ..functions.visemes import VisemePreview
|
|
from ..functions.eye_tracking import set_rotation
|
|
|
|
class ValidationMessageItem(PropertyGroup):
|
|
"""Property group for validation message items"""
|
|
name: StringProperty(name="Message")
|
|
|
|
class ZeroWeightBoneItem(PropertyGroup):
|
|
"""Property group for zero weight bone list items"""
|
|
name: StringProperty(name="Bone Name")
|
|
selected: BoolProperty(name="Selected", default=True)
|
|
has_children: BoolProperty(name="Has Children", default=False)
|
|
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
|
|
|
|
|
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
|
"""Updates validation mode and saves preference"""
|
|
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
|
save_preference("validation_mode", self.validation_mode)
|
|
|
|
|
|
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
|
"""Updates logging state and configures logging"""
|
|
logger.info(f"Updating logging state to: {self.enable_logging}")
|
|
save_preference("enable_logging", self.enable_logging)
|
|
from .logging_setup import configure_logging
|
|
configure_logging(self.enable_logging)
|
|
|
|
|
|
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
|
|
"""Updates shape key intensity and refreshes preview"""
|
|
if self.viseme_preview_mode:
|
|
VisemePreview.update_preview(context)
|
|
|
|
|
|
def highlight_problem_bones(self: PropertyGroup, context: Context) -> None:
|
|
"""Updates problem bone highlighting state and saves preference"""
|
|
logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}")
|
|
save_preference("highlight_problem_bones", self.highlight_problem_bones)
|
|
|
|
|
|
class AvatarToolkitSceneProperties(PropertyGroup):
|
|
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
|
|
|
show_found_bones: BoolProperty(
|
|
name="Show Found Bones",
|
|
default=False
|
|
)
|
|
|
|
show_non_standard: BoolProperty(
|
|
name="Show Non-Standard Bones",
|
|
default=False
|
|
)
|
|
|
|
show_hierarchy: BoolProperty(
|
|
name="Show Hierarchy Issues",
|
|
default=False
|
|
)
|
|
|
|
material_search_filter: StringProperty(
|
|
name=t("TextureAtlas.search_materials"),
|
|
description=t("TextureAtlas.search_materials_desc"),
|
|
default=""
|
|
)
|
|
|
|
def get_texture_node_list(self: Material, context: Context) -> list[tuple]:
|
|
if self.use_nodes:
|
|
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
|
(i.image.name if i.image else "node with no image..."),
|
|
(i.image.name if i.image else i.name), index+1)
|
|
for index, i in enumerate(self.node_tree.nodes)
|
|
if i.bl_idname == "ShaderNodeTexImage"]
|
|
if not len(Object.Enum):
|
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
|
t("TextureAtlas.no_images_error.desc"),
|
|
t("TextureAtlas.error.label"), 0)]
|
|
else:
|
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
|
t("TextureAtlas.no_nodes_error.desc"),
|
|
t("TextureAtlas.error.label"), 0)]
|
|
Object.Enum.append((t("TextureAtlas.none.label"),
|
|
t("TextureAtlas.none.label"),
|
|
t("TextureAtlas.none.label"), 0))
|
|
return Object.Enum
|
|
|
|
Material.texture_atlas_albedo = EnumProperty(
|
|
name=t("TextureAtlas.albedo"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.texture_atlas_normal = EnumProperty(
|
|
name=t("TextureAtlas.normal"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.texture_atlas_emission = EnumProperty(
|
|
name=t("TextureAtlas.emission"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.texture_atlas_ambient_occlusion = EnumProperty(
|
|
name=t("TextureAtlas.ambient_occlusion"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.texture_atlas_height = EnumProperty(
|
|
name=t("TextureAtlas.height"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.texture_atlas_roughness = EnumProperty(
|
|
name=t("TextureAtlas.roughness"),
|
|
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
|
|
default=0,
|
|
items=get_texture_node_list
|
|
)
|
|
|
|
Material.include_in_atlas = BoolProperty(
|
|
name=t("TextureAtlas.include_in_atlas"),
|
|
description=t("TextureAtlas.include_in_atlas_desc"),
|
|
default=False
|
|
)
|
|
|
|
Material.material_expanded = BoolProperty(
|
|
name=t("TextureAtlas.material_expanded"),
|
|
description=t("TextureAtlas.material_expanded_desc"),
|
|
default=False
|
|
)
|
|
|
|
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
|
name=t("TextureAtlas.list_shown"),
|
|
description=t("TextureAtlas.list_shown_desc"),
|
|
default=False
|
|
)
|
|
|
|
texture_atlas_material_index: IntProperty(
|
|
default=-1,
|
|
get=lambda self: -1,
|
|
set=lambda self, context: None
|
|
)
|
|
|
|
materials: CollectionProperty(
|
|
type=SceneMatClass
|
|
)
|
|
|
|
avatar_toolkit_updater_version_list: EnumProperty(
|
|
items=get_version_list,
|
|
name=t("Scene.avatar_toolkit_updater_version_list.name"),
|
|
description=t("Scene.avatar_toolkit_updater_version_list.description")
|
|
)
|
|
|
|
active_armature: EnumProperty(
|
|
items=get_armature_list,
|
|
name=t("QuickAccess.select_armature"),
|
|
description=t("QuickAccess.select_armature"),
|
|
)
|
|
|
|
language: EnumProperty(
|
|
name=t("Settings.language"),
|
|
description=t("Settings.language_desc"),
|
|
items=get_languages_list,
|
|
update=update_language
|
|
)
|
|
|
|
validation_mode: EnumProperty(
|
|
name=t("Settings.validation_mode"),
|
|
description=t("Settings.validation_mode_desc"),
|
|
items=[
|
|
('STRICT', t("Settings.validation_mode.strict"), t("Settings.validation_mode.strict_desc")),
|
|
('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")),
|
|
('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc"))
|
|
],
|
|
default=get_preference("validation_mode", "STRICT"),
|
|
update=update_validation_mode
|
|
)
|
|
|
|
enable_logging: BoolProperty(
|
|
name=t("Settings.enable_logging"),
|
|
description=t("Settings.enable_logging_desc"),
|
|
default=False,
|
|
update=update_logging_state
|
|
)
|
|
|
|
debug_expand: BoolProperty(
|
|
name="Debug Settings Expanded",
|
|
default=False
|
|
)
|
|
|
|
remove_doubles_merge_distance: FloatProperty(
|
|
name=t("Optimization.merge_distance"),
|
|
description=t("Optimization.merge_distance_desc"),
|
|
default=0.0001,
|
|
min=0.00001,
|
|
max=0.1
|
|
)
|
|
|
|
remove_doubles_advanced: BoolProperty(
|
|
name=t("Optimization.remove_doubles_advanced"),
|
|
description=t("Optimization.remove_doubles_advanced_desc"),
|
|
default=False
|
|
)
|
|
|
|
connect_bones_min_distance: FloatProperty(
|
|
name=t("Tools.connect_bones_min_distance"),
|
|
description=t("Tools.connect_bones_min_distance_desc"),
|
|
default=0.001,
|
|
min=0.0001,
|
|
max=0.1,
|
|
precision=4
|
|
)
|
|
|
|
merge_twist_bones: BoolProperty(
|
|
name=t("MMD.merge_twist_bones"),
|
|
description=t("MMD.merge_twist_bones_desc"),
|
|
default=True
|
|
)
|
|
|
|
keep_twist_bones: BoolProperty(
|
|
name=t("MMD.keep_twist_bones"),
|
|
description=t("MMD.keep_twist_bones_desc"),
|
|
default=False
|
|
)
|
|
|
|
keep_upper_chest: BoolProperty(
|
|
name=t("MMD.keep_upper_chest"),
|
|
description=t("MMD.keep_upper_chest_desc"),
|
|
default=True
|
|
)
|
|
|
|
merge_weights_threshold: FloatProperty(
|
|
name=t("MMD.merge_weights_threshold"),
|
|
description=t("MMD.merge_weights_threshold_desc"),
|
|
default=0.01,
|
|
min=0.0,
|
|
max=1.0
|
|
)
|
|
|
|
viseme_preview_mode: BoolProperty(
|
|
name=t("Visemes.preview_mode"),
|
|
description=t("Visemes.preview_mode_desc"),
|
|
default=False
|
|
)
|
|
|
|
mouth_a: StringProperty(
|
|
name=t("Visemes.mouth_a"),
|
|
description=t("Visemes.mouth_a_desc")
|
|
)
|
|
|
|
mouth_o: StringProperty(
|
|
name=t("Visemes.mouth_o"),
|
|
description=t("Visemes.mouth_o_desc")
|
|
)
|
|
|
|
mouth_ch: StringProperty(
|
|
name=t("Visemes.mouth_ch"),
|
|
description=t("Visemes.mouth_ch_desc")
|
|
)
|
|
|
|
viseme_mesh: StringProperty(
|
|
name=t("Visemes.mesh_select"),
|
|
description=t("Visemes.mesh_select_desc"),
|
|
)
|
|
|
|
shape_intensity: FloatProperty(
|
|
name=t("Visemes.shape_intensity"),
|
|
description=t("Visemes.shape_intensity_desc"),
|
|
default=1.0,
|
|
min=0.0,
|
|
max=2.0,
|
|
precision=3,
|
|
update=update_shape_intensity
|
|
)
|
|
|
|
viseme_preview_selection: EnumProperty(
|
|
name=t("Visemes.preview_selection"),
|
|
description=t("Visemes.preview_selection_desc"),
|
|
items=[
|
|
('vrc.v_aa', 'AA', 'A as in "bat"'),
|
|
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
|
|
('vrc.v_dd', 'DD', 'D as in "dog"'),
|
|
('vrc.v_ih', 'IH', 'I as in "bit"'),
|
|
('vrc.v_ff', 'FF', 'F as in "fox"'),
|
|
('vrc.v_e', 'E', 'E as in "bet"'),
|
|
('vrc.v_kk', 'KK', 'K as in "cat"'),
|
|
('vrc.v_nn', 'NN', 'N as in "net"'),
|
|
('vrc.v_oh', 'OH', 'O as in "hot"'),
|
|
('vrc.v_ou', 'OU', 'O as in "go"'),
|
|
('vrc.v_pp', 'PP', 'P as in "pat"'),
|
|
('vrc.v_rr', 'RR', 'R as in "red"'),
|
|
('vrc.v_sil', 'SIL', 'Silence'),
|
|
('vrc.v_ss', 'SS', 'S as in "sit"'),
|
|
('vrc.v_th', 'TH', 'Th as in "think"')
|
|
],
|
|
update=lambda s, c: VisemePreview.update_preview(c)
|
|
)
|
|
|
|
eye_tracking_type: EnumProperty(
|
|
name=t("EyeTracking.type"),
|
|
description=t("EyeTracking.type_desc"),
|
|
items=[
|
|
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
|
|
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
|
],
|
|
default='AV3'
|
|
)
|
|
|
|
eye_mode: EnumProperty(
|
|
name=t("EyeTracking.mode"),
|
|
items=[
|
|
('CREATION', t("EyeTracking.mode.creation"), ""),
|
|
('TESTING', t("EyeTracking.mode.testing"), "")
|
|
],
|
|
default='CREATION'
|
|
)
|
|
|
|
eye_rotation_x: FloatProperty(
|
|
name=t("EyeTracking.rotation.x"),
|
|
update=set_rotation
|
|
)
|
|
|
|
eye_rotation_y: FloatProperty(
|
|
name=t("EyeTracking.rotation.y"),
|
|
update=set_rotation
|
|
)
|
|
|
|
mesh_name_eye: StringProperty(
|
|
name=t("EyeTracking.mesh_name"),
|
|
description=t("EyeTracking.mesh_name_desc")
|
|
)
|
|
|
|
head: StringProperty(
|
|
name=t("EyeTracking.head_bone"),
|
|
description=t("EyeTracking.head_bone_desc")
|
|
)
|
|
|
|
eye_left: StringProperty(
|
|
name=t("EyeTracking.eye_left"),
|
|
description=t("EyeTracking.eye_left_desc")
|
|
)
|
|
|
|
eye_right: StringProperty(
|
|
name=t("EyeTracking.eye_right"),
|
|
description=t("EyeTracking.eye_right_desc")
|
|
)
|
|
|
|
disable_eye_movement: BoolProperty(
|
|
name=t("EyeTracking.disable_movement"),
|
|
description=t("EyeTracking.disable_movement_desc"),
|
|
default=False
|
|
)
|
|
|
|
disable_eye_blinking: BoolProperty(
|
|
name=t("EyeTracking.disable_blinking"),
|
|
description=t("EyeTracking.disable_blinking_desc"),
|
|
default=False
|
|
)
|
|
|
|
eye_distance: FloatProperty(
|
|
name=t("EyeTracking.distance"),
|
|
description=t("EyeTracking.distance_desc"),
|
|
default=0.0,
|
|
min=-1.0,
|
|
max=1.0
|
|
)
|
|
|
|
iris_height: FloatProperty(
|
|
name=t("EyeTracking.iris_height"),
|
|
description=t("EyeTracking.iris_height_desc"),
|
|
default=0.0,
|
|
min=-1.0,
|
|
max=1.0
|
|
)
|
|
|
|
eye_blink_shape: FloatProperty(
|
|
name=t("EyeTracking.blink_shape"),
|
|
description=t("EyeTracking.blink_shape_desc"),
|
|
default=1.0,
|
|
min=0.0,
|
|
max=1.0
|
|
)
|
|
|
|
eye_lowerlid_shape: FloatProperty(
|
|
name=t("EyeTracking.lowerlid_shape"),
|
|
description=t("EyeTracking.lowerlid_shape_desc"),
|
|
default=1.0,
|
|
min=0.0,
|
|
max=1.0
|
|
)
|
|
|
|
wink_left: StringProperty(
|
|
name=t("EyeTracking.wink_left"),
|
|
description=t("EyeTracking.wink_left_desc")
|
|
)
|
|
|
|
wink_right: StringProperty(
|
|
name=t("EyeTracking.wink_right"),
|
|
description=t("EyeTracking.wink_right_desc")
|
|
)
|
|
|
|
lowerlid_left: StringProperty(
|
|
name=t("EyeTracking.lowerlid_left"),
|
|
description=t("EyeTracking.lowerlid_left_desc")
|
|
)
|
|
|
|
lowerlid_right: StringProperty(
|
|
name=t("EyeTracking.lowerlid_right"),
|
|
description=t("EyeTracking.lowerlid_right_desc")
|
|
)
|
|
|
|
merge_mode: EnumProperty(
|
|
name=t('CustomPanel.merge_mode'),
|
|
description=t('CustomPanel.merge_mode_desc'),
|
|
items=[
|
|
('ARMATURE', t('CustomPanel.mode.armature'), t('CustomPanel.mode.armature_desc')),
|
|
('MESH', t('CustomPanel.mode.mesh'), t('CustomPanel.mode.mesh_desc'))
|
|
],
|
|
default='ARMATURE'
|
|
)
|
|
|
|
merge_armature_into: StringProperty(
|
|
name=t('MergeArmature.into'),
|
|
description=t('MergeArmature.into_desc'),
|
|
default=""
|
|
)
|
|
|
|
merge_armature: StringProperty(
|
|
name=t('MergeArmature.from'),
|
|
description=t('MergeArmature.from_desc'),
|
|
default=""
|
|
)
|
|
|
|
attach_mesh: StringProperty(
|
|
name=t('AttachMesh.select'),
|
|
description=t('AttachMesh.select_desc'),
|
|
default=""
|
|
)
|
|
|
|
attach_bone: StringProperty(
|
|
name=t('AttachBone.select'),
|
|
description=t('AttachBone.select_desc'),
|
|
default=""
|
|
)
|
|
|
|
apply_transforms: BoolProperty(
|
|
name=t('MergeArmature.apply_transforms'),
|
|
description=t('MergeArmature.apply_transforms_desc'),
|
|
default=True
|
|
)
|
|
|
|
join_meshes: BoolProperty(
|
|
name=t('MergeArmature.join_meshes'),
|
|
description=t('MergeArmature.join_meshes_desc'),
|
|
default=True
|
|
)
|
|
|
|
remove_zero_weights: BoolProperty(
|
|
name=t('MergeArmature.remove_zero_weights'),
|
|
description=t('MergeArmature.remove_zero_weights_desc'),
|
|
default=True
|
|
)
|
|
|
|
preserve_parent_bones: BoolProperty(
|
|
name=t("Tools.preserve_parent_bones"),
|
|
description=t("Tools.preserve_parent_bones_desc"),
|
|
default=True
|
|
)
|
|
|
|
target_bone_type: EnumProperty(
|
|
name=t("Tools.target_bone_type"),
|
|
description=t("Tools.target_bone_type_desc"),
|
|
items=[
|
|
('ALL', t("Tools.target_all_bones"), ""),
|
|
('DEFORM', t("Tools.target_deform_bones"), ""),
|
|
('NON_DEFORM', t("Tools.target_non_deform_bones"), "")
|
|
],
|
|
default='ALL'
|
|
)
|
|
|
|
zero_weight_bones: CollectionProperty(
|
|
type=ZeroWeightBoneItem,
|
|
name="Zero Weight Bones",
|
|
description="List of bones with zero weights"
|
|
)
|
|
|
|
zero_weight_bones_index: IntProperty(
|
|
name="Zero Weight Bone Index",
|
|
default=0
|
|
)
|
|
|
|
list_only_mode: BoolProperty(
|
|
name=t("Tools.list_only_mode"),
|
|
description=t("Tools.list_only_mode_desc"),
|
|
default=False
|
|
)
|
|
|
|
cleanup_shape_keys: BoolProperty(
|
|
name=t('MergeArmature.cleanup_shape_keys'),
|
|
description=t('MergeArmature.cleanup_shape_keys_desc'),
|
|
default=True
|
|
)
|
|
|
|
merge_twist_bones: BoolProperty(
|
|
name=t("Tools.merge_twist_bones"),
|
|
description=t("Tools.merge_twist_bones_desc"),
|
|
default=True
|
|
)
|
|
|
|
highlight_problem_bones: BoolProperty(
|
|
name=t("Settings.highlight_problem_bones"),
|
|
description=t("Settings.highlight_problem_bones_desc"),
|
|
default=get_preference("highlight_problem_bones", True),
|
|
update=highlight_problem_bones
|
|
)
|
|
|
|
show_scale_issues: BoolProperty(
|
|
name="Show Scale Issues",
|
|
default=False
|
|
)
|
|
|
|
tpose_validation_result: BoolProperty(
|
|
name="T-Pose Validation Result",
|
|
default=True
|
|
)
|
|
|
|
tpose_validation_messages: CollectionProperty(
|
|
type=bpy.types.PropertyGroup,
|
|
name="T-Pose Validation Messages"
|
|
)
|
|
|
|
show_tpose_validation: BoolProperty(
|
|
name="Show T-Pose Validation Results",
|
|
default=False
|
|
)
|
|
|
|
def register() -> None:
|
|
"""Register the Avatar Toolkit property group"""
|
|
logger.info("Registering Avatar Toolkit properties")
|
|
|
|
# Clear any existing registrations to prevent conflicts
|
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
|
try:
|
|
del bpy.types.Scene.avatar_toolkit
|
|
except:
|
|
logger.warning("Failed to remove existing avatar_toolkit property")
|
|
|
|
# Register classes
|
|
try:
|
|
# Try to register all classes at once
|
|
bpy.utils.register_class(ZeroWeightBoneItem)
|
|
bpy.utils.register_class(ValidationMessageItem)
|
|
bpy.utils.register_class(AvatarToolkitSceneProperties)
|
|
except ValueError as e:
|
|
logger.warning(f"Class registration issue: {e}")
|
|
# Try to unregister first in case they're already registered
|
|
try:
|
|
# Try to unregister in reverse order
|
|
try:
|
|
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
|
except:
|
|
pass
|
|
try:
|
|
bpy.utils.unregister_class(ValidationMessageItem)
|
|
except:
|
|
pass
|
|
try:
|
|
bpy.utils.unregister_class(ZeroWeightBoneItem)
|
|
except:
|
|
pass
|
|
|
|
# Then register again
|
|
bpy.utils.register_class(ZeroWeightBoneItem)
|
|
bpy.utils.register_class(ValidationMessageItem)
|
|
bpy.utils.register_class(AvatarToolkitSceneProperties)
|
|
except Exception as e:
|
|
logger.error(f"Failed to recover from registration error: {e}")
|
|
raise
|
|
|
|
# Register the property
|
|
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
|
logger.debug("Properties registered successfully")
|
|
|
|
|
|
def unregister() -> None:
|
|
"""Unregister the Avatar Toolkit property group"""
|
|
logger.info("Unregistering Avatar Toolkit properties")
|
|
|
|
# Remove the property first
|
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
|
try:
|
|
del bpy.types.Scene.avatar_toolkit
|
|
logger.debug("Removed avatar_toolkit property")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to remove avatar_toolkit property: {e}")
|
|
|
|
# Then unregister the classes
|
|
try:
|
|
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
|
bpy.utils.unregister_class(ValidationMessageItem)
|
|
bpy.utils.unregister_class(ZeroWeightBoneItem)
|
|
logger.debug("Unregistered property classes")
|
|
except (RuntimeError, ValueError) as e:
|
|
logger.warning(f"Error during property class unregistration: {e}")
|
|
# Not fatal - continue
|