From 771c4926a61436afc8374d8b0e0c1827844350af Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 25 Jul 2024 23:06:20 +0100 Subject: [PATCH] Progress System - Added Progress system so the user knows something is being done when there use certain functions. Currently only Join Meshes, Viseme creation and combine materials use it. - Disbabled some translation debguing stuff to remove spam from the console. --- core/common.py | 19 +++++++++++++- core/properties.py | 3 +++ functions/combine_materials.py | 15 ++++++++++- functions/join_meshes.py | 21 ++++++++++++++- functions/translations.py | 14 +++++----- functions/viseme.py | 43 ++++++++++++++----------------- resources/translations/en_US.json | 12 +++++++++ 7 files changed, 94 insertions(+), 33 deletions(-) diff --git a/core/common.py b/core/common.py index 9e5818e..0ea4ae0 100644 --- a/core/common.py +++ b/core/common.py @@ -6,10 +6,11 @@ import time import webbrowser import typing +from ..core.register import register_wrap from typing import List, Optional, Tuple from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup from functools import lru_cache -from bpy.props import PointerProperty +from bpy.props import PointerProperty, IntProperty, StringProperty from bpy.utils import register_class @@ -245,3 +246,19 @@ def remove_default_objects(): for obj in bpy.data.objects: if obj.name in ["Camera", "Light", "Cube"]: bpy.data.objects.remove(obj, do_unlink=True) + +def init_progress(context, steps): + context.window_manager.progress_begin(0, 100) + context.scene.avatar_toolkit_progress_steps = steps + context.scene.avatar_toolkit_progress_current = 0 + +def update_progress(self, context, message): + context.scene.avatar_toolkit_progress_current += 1 + progress = (context.scene.avatar_toolkit_progress_current / context.scene.avatar_toolkit_progress_steps) * 100 + context.window_manager.progress_update(progress) + context.area.header_text_set(message) + self.report({'INFO'}, message) + +def finish_progress(context): + context.window_manager.progress_end() + context.area.header_text_set(None) diff --git a/core/properties.py b/core/properties.py index 2a00b5d..8eda6a6 100644 --- a/core/properties.py +++ b/core/properties.py @@ -26,6 +26,9 @@ def register() -> None: bpy.types.Scene.avatar_toolkit_language_changed = bpy.props.BoolProperty(default=False) + bpy.types.Scene.avatar_toolkit_progress_steps = bpy.props.IntProperty(default=0) + bpy.types.Scene.avatar_toolkit_progress_current = bpy.props.IntProperty(default=0) + bpy.types.Scene.mouth_a = bpy.props.StringProperty( name=t("VisemePanel.mouth_a.label"), description=t("VisemePanel.mouth_a.desc") diff --git a/functions/combine_materials.py b/functions/combine_materials.py index 705de86..e58056d 100644 --- a/functions/combine_materials.py +++ b/functions/combine_materials.py @@ -2,7 +2,7 @@ import bpy import re from typing import List, Tuple, Optional, Set, Dict from bpy.types import Material, Operator, Context, Object, NodeTree -from ..core.common import clean_material_names, get_selected_armature, is_valid_armature, get_all_meshes +from ..core.common import clean_material_names, get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress from ..core.register import register_wrap from ..functions.translations import t @@ -78,11 +78,23 @@ class CombineMaterials(Operator): self.report({'WARNING'}, t("Optimization.no_meshes_found")) return {'CANCELLED'} + init_progress(context, 5) # 5 steps in total + + update_progress(self, context, t("Optimization.consolidating_materials")) self.consolidate_materials(meshes) + + update_progress(self, context, t("Optimization.cleaning_material_slots")) self.clean_material_slots(meshes) + + update_progress(self, context, t("Optimization.cleaning_material_names")) self.clean_material_names() + + update_progress(self, context, t("Optimization.clearing_unused_data")) self.clear_unused_data_blocks() + update_progress(self, context, t("Optimization.finalizing")) + finish_progress(context) + return {'FINISHED'} def consolidate_materials(self, meshes: List[Object]) -> None: @@ -123,3 +135,4 @@ class CombineMaterials(Operator): def clear_unused_data_blocks(self) -> None: bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + diff --git a/functions/join_meshes.py b/functions/join_meshes.py index 6a4e3d2..d3f74b9 100644 --- a/functions/join_meshes.py +++ b/functions/join_meshes.py @@ -2,7 +2,7 @@ import bpy from typing import List, Optional, Set from bpy.types import Operator, Context, Object from ..core.register import register_wrap -from ..core.common import fix_uv_coordinates, get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes +from ..core.common import fix_uv_coordinates, get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes, init_progress, update_progress, finish_progress from ..functions.translations import t @register_wrap @@ -37,22 +37,31 @@ class JoinAllMeshes(Operator): if not meshes: raise ValueError(t("Optimization.no_meshes_found")) + init_progress(context, 5) # 5 steps in total + + update_progress(self, context, t("Optimization.selecting_meshes")) for mesh in meshes: mesh.select_set(True) if bpy.context.selected_objects: bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] + + update_progress(self, context, t("Optimization.joining_meshes")) try: bpy.ops.object.join() except RuntimeError as e: raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}") + update_progress(self, context, t("Optimization.applying_transforms")) try: bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) except RuntimeError as e: raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}") + update_progress(self, context, t("Optimization.fixing_uv_coordinates")) fix_uv_coordinates(context) + + update_progress(self, context, t("Optimization.finalizing")) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') self.report({'INFO'}, t("Optimization.meshes_joined")) @@ -60,6 +69,7 @@ class JoinAllMeshes(Operator): raise ValueError(t("Optimization.no_mesh_selected")) context.view_layer.objects.active = armature + finish_progress(context) @register_wrap class JoinSelectedMeshes(Operator): @@ -86,24 +96,32 @@ class JoinSelectedMeshes(Operator): if len(selected_objects) < 2: raise ValueError(t("Optimization.select_at_least_two_meshes")) + init_progress(context, 5) # 5 steps in total + + update_progress(self, context, t("Optimization.preparing_meshes")) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') + update_progress(self, context, t("Optimization.selecting_meshes")) for obj in selected_objects: obj.select_set(True) if bpy.context.selected_objects: bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] + + update_progress(self, context, t("Optimization.joining_meshes")) try: bpy.ops.object.join() except RuntimeError as e: raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}") + update_progress(self, context, t("Optimization.applying_transforms")) try: bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) except RuntimeError as e: raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}") + update_progress(self, context, t("Optimization.fixing_uv_coordinates")) fix_uv_coordinates(context) bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') @@ -111,3 +129,4 @@ class JoinSelectedMeshes(Operator): else: raise ValueError(t("Optimization.no_mesh_selected")) + finish_progress(context) diff --git a/functions/translations.py b/functions/translations.py index 9e2a742..e5867b6 100644 --- a/functions/translations.py +++ b/functions/translations.py @@ -30,7 +30,7 @@ def load_translations() -> bool: languages.append(lang) language_index = get_preference("language", 0) - print(f"Loading translations for language index: {language_index}") # Debug print + # print(f"Loading translations for language index: {language_index}") # Debug print if language_index == 0: # "auto" language = bpy.context.preferences.view.language @@ -40,27 +40,27 @@ def load_translations() -> bool: except IndexError: language = bpy.context.preferences.view.language - print(f"Selected language: {language}") # Debug print + # print(f"Selected language: {language}") # Debug print translation_file: str = os.path.join(translations_dir, language + ".json") if os.path.exists(translation_file): with open(translation_file, 'r', encoding='utf-8') as file: dictionary = json.load(file)["messages"] - print(f"Loaded translations: {dictionary}") # Debug print + # print(f"Loaded translations: {dictionary}") # Debug print else: custom_language: str = language.split("_")[0] custom_translation_file: str = os.path.join(translations_dir, custom_language + ".json") if os.path.exists(custom_translation_file): with open(custom_translation_file, 'r', encoding='utf-8') as file: dictionary = json.load(file)["messages"] - print(f"Loaded custom translations: {dictionary}") # Debug print + # print(f"Loaded custom translations: {dictionary}") # Debug print else: print(f"Translation file not found for language: {language}") default_file: str = os.path.join(translations_dir, "en_US.json") if os.path.exists(default_file): with open(default_file, 'r', encoding='utf-8') as file: dictionary = json.load(file)["messages"] - print(f"Loaded default translations: {dictionary}") # Debug print + # print(f"Loaded default translations: {dictionary}") # Debug print else: print("Default translation file 'en_US.json' not found.") @@ -72,7 +72,7 @@ def t(phrase: str, default: str = None) -> str: if verbose: print(f'Warning: Unknown phrase: {phrase}') return default if default is not None else phrase - print(f"Translating '{phrase}' to '{output}'") # Debug print + # print(f"Translating '{phrase}' to '{output}'") # Debug print return output def get_language_display_name(lang: str) -> str: @@ -93,5 +93,5 @@ def update_language(self, context): bpy.ops.avatar_toolkit.translation_restart_popup('INVOKE_DEFAULT') # Initial load of translations -print("Performing initial load of translations") # Debug print +# print("Performing initial load of translations") # Debug print load_translations() diff --git a/functions/viseme.py b/functions/viseme.py index 871ce73..8e32146 100644 --- a/functions/viseme.py +++ b/functions/viseme.py @@ -3,7 +3,7 @@ from ..core import common from ..core.register import register_wrap from ..functions.translations import t from typing import List, Tuple -from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes +from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress @register_wrap class AutoVisemeButton(bpy.types.Operator): @@ -18,26 +18,32 @@ class AutoVisemeButton(bpy.types.Operator): return armature is not None and is_valid_armature(armature) and get_all_meshes(context) def execute(self, context: bpy.types.Context) -> set: - print("Starting viseme creation...") - mesh = bpy.data.objects.get(context.scene.selected_mesh) - if not mesh or not common.has_shapekeys(mesh): - self.report({'ERROR'}, t('AutoVisemeButton.error.noShapekeys')) + try: + self.create_visemes(context) + return {'FINISHED'} + except Exception as e: + self.report({'ERROR'}, str(e)) return {'CANCELLED'} - # Remove existing VRC shape keys + def create_visemes(self, context: bpy.types.Context) -> None: + init_progress(context, 5) # 5 main steps + + update_progress(self, context, t("VisemePanel.start_viseme_creation")) + mesh = bpy.data.objects.get(context.scene.selected_mesh) + if not mesh or not common.has_shapekeys(mesh): + raise ValueError(t('AutoVisemeButton.error.noShapekeys')) + + update_progress(self, context, t("VisemePanel.removing_existing_visemes")) self.remove_existing_vrc_shapekeys(mesh) shape_a = context.scene.mouth_a shape_o = context.scene.mouth_o shape_ch = context.scene.mouth_ch - print(f"Selected shapes: A={shape_a}, O={shape_o}, CH={shape_ch}") - if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis": - self.report({'ERROR'}, t('AutoVisemeButton.error.selectShapekeys')) - return {'CANCELLED'} + raise ValueError(t('AutoVisemeButton.error.selectShapekeys')) - # Create visemes + update_progress(self, context, t("VisemePanel.creating_visemes")) visemes: List[Tuple[str, List[Tuple[str, float]]]] = [ ('vrc.v_aa', [(shape_a, 0.9998)]), ('vrc.v_ch', [(shape_ch, 0.9996)]), @@ -57,38 +63,29 @@ class AutoVisemeButton(bpy.types.Operator): ] for viseme_name, shape_mix in visemes: - print(f"Creating viseme: {viseme_name}") self.create_viseme(mesh, viseme_name, shape_mix, context.scene.shape_intensity) - print("Sorting shape keys...") + update_progress(self, context, t("VisemePanel.sorting_shapekeys")) common.sort_shape_keys(mesh) - self.report({'INFO'}, t('AutoVisemeButton.success')) - return {'FINISHED'} + update_progress(self, context, t("VisemePanel.viseme_creation_completed")) + finish_progress(context) def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None: - print(f" Creating viseme: {viseme_name}") shape_keys = mesh.data.shape_keys.key_blocks - # Remove existing viseme if it exists if viseme_name in shape_keys: - print(f" Removing existing viseme: {viseme_name}") mesh.shape_key_remove(shape_keys[viseme_name]) - # Create new viseme new_key = mesh.shape_key_add(name=viseme_name, from_mix=False) new_key.value = 0.0 - # Mix shapes for shape_name, value in shape_mix: if shape_name in shape_keys: source_shape = shape_keys[shape_name] - print(f" Mixing shape: {shape_name} with value: {value * intensity}") for i, vert in enumerate(new_key.data): vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity - print(f" Viseme {viseme_name} created successfully.") - def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None: vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_'] shape_keys = mesh.data.shape_keys.key_blocks diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 35b5095..adbec50 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -56,6 +56,16 @@ "Optimization.join_error": "Error during mesh joining", "Optimization.join_operation_failed": "Join operation failed", "Optimization.transform_apply_failed": "Transform apply failed", + "Optimization.selecting_meshes": "Selecting meshes...", + "Optimization.joining_meshes": "Joining meshes...", + "Optimization.applying_transforms": "Applying transforms...", + "Optimization.fixing_uv_coordinates": "Fixing UV coordinates...", + "Optimization.finalizing": "Finalizing...", + "Optimization.preparing_meshes": "Preparing meshes...", + "Optimization.consolidating_materials": "Consolidating materials...", + "Optimization.cleaning_material_slots": "Cleaning material slots...", + "Optimization.cleaning_material_names": "Cleaning material names...", + "Optimization.clearing_unused_data": "Clearing unused data...", "Tools.select_armature": "Please select an armature", "Tools.label": "Tools", "Tools.tools_title.label": "Tools:", @@ -102,6 +112,8 @@ "VisemePanel.removing_existing_viseme": "Removing existing viseme: {viseme_name}", "VisemePanel.mixing_shape": "Mixing shape: {shape_name} with value: {value}", "VisemePanel.viseme_created_successfully": "Viseme {viseme_name} created successfully", + "VisemePanel.removing_existing_visemes": "Removing existing visemes...", + "VisemePanel.creating_visemes": "Creating visemes...", "AutoVisemeButton.label": "Create Visemes", "AutoVisemeButton.desc": "Create visemes automatically, based on shape keys", "AutoVisemeButton.error.noShapekeys": "No shape keys found",