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.
This commit is contained in:
Yusarina
2024-07-25 23:06:20 +01:00
parent fef46cd567
commit 771c4926a6
7 changed files with 94 additions and 33 deletions
+18 -1
View File
@@ -6,10 +6,11 @@ import time
import webbrowser import webbrowser
import typing import typing
from ..core.register import register_wrap
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup
from functools import lru_cache from functools import lru_cache
from bpy.props import PointerProperty from bpy.props import PointerProperty, IntProperty, StringProperty
from bpy.utils import register_class from bpy.utils import register_class
@@ -245,3 +246,19 @@ def remove_default_objects():
for obj in bpy.data.objects: for obj in bpy.data.objects:
if obj.name in ["Camera", "Light", "Cube"]: if obj.name in ["Camera", "Light", "Cube"]:
bpy.data.objects.remove(obj, do_unlink=True) 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)
+3
View File
@@ -26,6 +26,9 @@ def register() -> None:
bpy.types.Scene.avatar_toolkit_language_changed = bpy.props.BoolProperty(default=False) 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( bpy.types.Scene.mouth_a = bpy.props.StringProperty(
name=t("VisemePanel.mouth_a.label"), name=t("VisemePanel.mouth_a.label"),
description=t("VisemePanel.mouth_a.desc") description=t("VisemePanel.mouth_a.desc")
+14 -1
View File
@@ -2,7 +2,7 @@ import bpy
import re import re
from typing import List, Tuple, Optional, Set, Dict from typing import List, Tuple, Optional, Set, Dict
from bpy.types import Material, Operator, Context, Object, NodeTree 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 ..core.register import register_wrap
from ..functions.translations import t from ..functions.translations import t
@@ -78,11 +78,23 @@ class CombineMaterials(Operator):
self.report({'WARNING'}, t("Optimization.no_meshes_found")) self.report({'WARNING'}, t("Optimization.no_meshes_found"))
return {'CANCELLED'} return {'CANCELLED'}
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.consolidating_materials"))
self.consolidate_materials(meshes) self.consolidate_materials(meshes)
update_progress(self, context, t("Optimization.cleaning_material_slots"))
self.clean_material_slots(meshes) self.clean_material_slots(meshes)
update_progress(self, context, t("Optimization.cleaning_material_names"))
self.clean_material_names() self.clean_material_names()
update_progress(self, context, t("Optimization.clearing_unused_data"))
self.clear_unused_data_blocks() self.clear_unused_data_blocks()
update_progress(self, context, t("Optimization.finalizing"))
finish_progress(context)
return {'FINISHED'} return {'FINISHED'}
def consolidate_materials(self, meshes: List[Object]) -> None: def consolidate_materials(self, meshes: List[Object]) -> None:
@@ -123,3 +135,4 @@ class CombineMaterials(Operator):
def clear_unused_data_blocks(self) -> None: def clear_unused_data_blocks(self) -> None:
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
+20 -1
View File
@@ -2,7 +2,7 @@ import bpy
from typing import List, Optional, Set from typing import List, Optional, Set
from bpy.types import Operator, Context, Object from bpy.types import Operator, Context, Object
from ..core.register import register_wrap 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 from ..functions.translations import t
@register_wrap @register_wrap
@@ -37,22 +37,31 @@ class JoinAllMeshes(Operator):
if not meshes: if not meshes:
raise ValueError(t("Optimization.no_meshes_found")) 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: for mesh in meshes:
mesh.select_set(True) mesh.select_set(True)
if bpy.context.selected_objects: if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
update_progress(self, context, t("Optimization.joining_meshes"))
try: try:
bpy.ops.object.join() bpy.ops.object.join()
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}") raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try: try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}") raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
fix_uv_coordinates(context) fix_uv_coordinates(context)
update_progress(self, context, t("Optimization.finalizing"))
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
self.report({'INFO'}, t("Optimization.meshes_joined")) self.report({'INFO'}, t("Optimization.meshes_joined"))
@@ -60,6 +69,7 @@ class JoinAllMeshes(Operator):
raise ValueError(t("Optimization.no_mesh_selected")) raise ValueError(t("Optimization.no_mesh_selected"))
context.view_layer.objects.active = armature context.view_layer.objects.active = armature
finish_progress(context)
@register_wrap @register_wrap
class JoinSelectedMeshes(Operator): class JoinSelectedMeshes(Operator):
@@ -86,24 +96,32 @@ class JoinSelectedMeshes(Operator):
if len(selected_objects) < 2: if len(selected_objects) < 2:
raise ValueError(t("Optimization.select_at_least_two_meshes")) 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.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
update_progress(self, context, t("Optimization.selecting_meshes"))
for obj in selected_objects: for obj in selected_objects:
obj.select_set(True) obj.select_set(True)
if bpy.context.selected_objects: if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
update_progress(self, context, t("Optimization.joining_meshes"))
try: try:
bpy.ops.object.join() bpy.ops.object.join()
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}") raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try: try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}") raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
fix_uv_coordinates(context) fix_uv_coordinates(context)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
@@ -111,3 +129,4 @@ class JoinSelectedMeshes(Operator):
else: else:
raise ValueError(t("Optimization.no_mesh_selected")) raise ValueError(t("Optimization.no_mesh_selected"))
finish_progress(context)
+7 -7
View File
@@ -30,7 +30,7 @@ def load_translations() -> bool:
languages.append(lang) languages.append(lang)
language_index = get_preference("language", 0) 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" if language_index == 0: # "auto"
language = bpy.context.preferences.view.language language = bpy.context.preferences.view.language
@@ -40,27 +40,27 @@ def load_translations() -> bool:
except IndexError: except IndexError:
language = bpy.context.preferences.view.language 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") translation_file: str = os.path.join(translations_dir, language + ".json")
if os.path.exists(translation_file): if os.path.exists(translation_file):
with open(translation_file, 'r', encoding='utf-8') as file: with open(translation_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"] dictionary = json.load(file)["messages"]
print(f"Loaded translations: {dictionary}") # Debug print # print(f"Loaded translations: {dictionary}") # Debug print
else: else:
custom_language: str = language.split("_")[0] custom_language: str = language.split("_")[0]
custom_translation_file: str = os.path.join(translations_dir, custom_language + ".json") custom_translation_file: str = os.path.join(translations_dir, custom_language + ".json")
if os.path.exists(custom_translation_file): if os.path.exists(custom_translation_file):
with open(custom_translation_file, 'r', encoding='utf-8') as file: with open(custom_translation_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"] dictionary = json.load(file)["messages"]
print(f"Loaded custom translations: {dictionary}") # Debug print # print(f"Loaded custom translations: {dictionary}") # Debug print
else: else:
print(f"Translation file not found for language: {language}") print(f"Translation file not found for language: {language}")
default_file: str = os.path.join(translations_dir, "en_US.json") default_file: str = os.path.join(translations_dir, "en_US.json")
if os.path.exists(default_file): if os.path.exists(default_file):
with open(default_file, 'r', encoding='utf-8') as file: with open(default_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"] dictionary = json.load(file)["messages"]
print(f"Loaded default translations: {dictionary}") # Debug print # print(f"Loaded default translations: {dictionary}") # Debug print
else: else:
print("Default translation file 'en_US.json' not found.") print("Default translation file 'en_US.json' not found.")
@@ -72,7 +72,7 @@ def t(phrase: str, default: str = None) -> str:
if verbose: if verbose:
print(f'Warning: Unknown phrase: {phrase}') print(f'Warning: Unknown phrase: {phrase}')
return default if default is not None else 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 return output
def get_language_display_name(lang: str) -> str: 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') bpy.ops.avatar_toolkit.translation_restart_popup('INVOKE_DEFAULT')
# Initial load of translations # Initial load of translations
print("Performing initial load of translations") # Debug print # print("Performing initial load of translations") # Debug print
load_translations() load_translations()
+20 -23
View File
@@ -3,7 +3,7 @@ from ..core import common
from ..core.register import register_wrap from ..core.register import register_wrap
from ..functions.translations import t from ..functions.translations import t
from typing import List, Tuple 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 @register_wrap
class AutoVisemeButton(bpy.types.Operator): 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) return armature is not None and is_valid_armature(armature) and get_all_meshes(context)
def execute(self, context: bpy.types.Context) -> set: def execute(self, context: bpy.types.Context) -> set:
print("Starting viseme creation...") try:
mesh = bpy.data.objects.get(context.scene.selected_mesh) self.create_visemes(context)
if not mesh or not common.has_shapekeys(mesh): return {'FINISHED'}
self.report({'ERROR'}, t('AutoVisemeButton.error.noShapekeys')) except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'} 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) self.remove_existing_vrc_shapekeys(mesh)
shape_a = context.scene.mouth_a shape_a = context.scene.mouth_a
shape_o = context.scene.mouth_o shape_o = context.scene.mouth_o
shape_ch = context.scene.mouth_ch 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": if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis":
self.report({'ERROR'}, t('AutoVisemeButton.error.selectShapekeys')) raise ValueError(t('AutoVisemeButton.error.selectShapekeys'))
return {'CANCELLED'}
# Create visemes update_progress(self, context, t("VisemePanel.creating_visemes"))
visemes: List[Tuple[str, List[Tuple[str, float]]]] = [ visemes: List[Tuple[str, List[Tuple[str, float]]]] = [
('vrc.v_aa', [(shape_a, 0.9998)]), ('vrc.v_aa', [(shape_a, 0.9998)]),
('vrc.v_ch', [(shape_ch, 0.9996)]), ('vrc.v_ch', [(shape_ch, 0.9996)]),
@@ -57,38 +63,29 @@ class AutoVisemeButton(bpy.types.Operator):
] ]
for viseme_name, shape_mix in visemes: 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) 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) common.sort_shape_keys(mesh)
self.report({'INFO'}, t('AutoVisemeButton.success')) update_progress(self, context, t("VisemePanel.viseme_creation_completed"))
return {'FINISHED'} finish_progress(context)
def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None: 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 shape_keys = mesh.data.shape_keys.key_blocks
# Remove existing viseme if it exists
if viseme_name in shape_keys: if viseme_name in shape_keys:
print(f" Removing existing viseme: {viseme_name}")
mesh.shape_key_remove(shape_keys[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 = mesh.shape_key_add(name=viseme_name, from_mix=False)
new_key.value = 0.0 new_key.value = 0.0
# Mix shapes
for shape_name, value in shape_mix: for shape_name, value in shape_mix:
if shape_name in shape_keys: if shape_name in shape_keys:
source_shape = shape_keys[shape_name] source_shape = shape_keys[shape_name]
print(f" Mixing shape: {shape_name} with value: {value * intensity}")
for i, vert in enumerate(new_key.data): for i, vert in enumerate(new_key.data):
vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity 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: def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None:
vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_'] vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_']
shape_keys = mesh.data.shape_keys.key_blocks shape_keys = mesh.data.shape_keys.key_blocks
+12
View File
@@ -56,6 +56,16 @@
"Optimization.join_error": "Error during mesh joining", "Optimization.join_error": "Error during mesh joining",
"Optimization.join_operation_failed": "Join operation failed", "Optimization.join_operation_failed": "Join operation failed",
"Optimization.transform_apply_failed": "Transform apply 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.select_armature": "Please select an armature",
"Tools.label": "Tools", "Tools.label": "Tools",
"Tools.tools_title.label": "Tools:", "Tools.tools_title.label": "Tools:",
@@ -102,6 +112,8 @@
"VisemePanel.removing_existing_viseme": "Removing existing viseme: {viseme_name}", "VisemePanel.removing_existing_viseme": "Removing existing viseme: {viseme_name}",
"VisemePanel.mixing_shape": "Mixing shape: {shape_name} with value: {value}", "VisemePanel.mixing_shape": "Mixing shape: {shape_name} with value: {value}",
"VisemePanel.viseme_created_successfully": "Viseme {viseme_name} created successfully", "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.label": "Create Visemes",
"AutoVisemeButton.desc": "Create visemes automatically, based on shape keys", "AutoVisemeButton.desc": "Create visemes automatically, based on shape keys",
"AutoVisemeButton.error.noShapekeys": "No shape keys found", "AutoVisemeButton.error.noShapekeys": "No shape keys found",