2026-03-11_4
This commit is contained in:
@@ -3,7 +3,7 @@ import bpy
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from . import retargeting
|
||||
from . import retargeting, utils
|
||||
from .auto_detect_lists.bones import bone_list, ignore_rokoko_retargeting_bones
|
||||
from .auto_detect_lists.shapes import shape_list
|
||||
from .custom_schemes_manager import load_custom_lists_from_file
|
||||
@@ -294,8 +294,12 @@ def detect_retarget_bones() -> {str: (str, str)}:
|
||||
armature_source = retargeting.get_source_armature()
|
||||
armature_target = retargeting.get_target_armature()
|
||||
|
||||
# Get all F-Curves from the source armature animation
|
||||
action = armature_source.animation_data.action
|
||||
fcurves = utils.get_fcurves_from_action(action)
|
||||
|
||||
# Get all source bones from the animation and add them to bone_list_animated
|
||||
for fc in armature_source.animation_data.action.fcurves:
|
||||
for fc in fcurves:
|
||||
bone_name = fc.data_path.split('"')
|
||||
if len(bone_name) == 3 and bone_name[1] not in bone_list_animated:
|
||||
bone_list_animated.append(bone_name[1])
|
||||
@@ -384,3 +388,4 @@ def detect_retarget_bones() -> {str: (str, str)}:
|
||||
break
|
||||
|
||||
return retargeting_dict
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import bpy
|
||||
import sys
|
||||
@@ -23,7 +23,7 @@ class LibraryManager:
|
||||
self.libs_info_file = self.libs_main_dir / ".lib_info"
|
||||
|
||||
python_ver_str = "".join([str(ver) for ver in sys.version_info[:2]])
|
||||
self.libs_dir = os.path.join(self.libs_main_dir, "python" + python_ver_str)
|
||||
self.libs_dir = os.path.join(self.libs_main_dir, f"python{python_ver_str}")
|
||||
|
||||
# Set python path on older Blender versions
|
||||
try:
|
||||
@@ -34,77 +34,114 @@ class LibraryManager:
|
||||
self.check_libs_info()
|
||||
self._prepare_libraries()
|
||||
|
||||
# ----------------- Helpers -----------------
|
||||
|
||||
@staticmethod
|
||||
def _extract_lib_name(lib: str) -> str:
|
||||
"""Remove version specifiers from requirement string (e.g. 'numpy>=1.20' -> 'numpy')."""
|
||||
for sep in ["=", "<", ">", "~"]:
|
||||
lib = lib.split(sep)[0]
|
||||
return lib.strip()
|
||||
|
||||
def _prepare_libraries(self):
|
||||
# Create main library directory
|
||||
if not os.path.isdir(self.libs_main_dir):
|
||||
os.mkdir(self.libs_main_dir)
|
||||
os.makedirs(self.libs_main_dir, exist_ok=True)
|
||||
# Create python specific library directory
|
||||
if not os.path.isdir(self.libs_dir):
|
||||
os.mkdir(self.libs_dir)
|
||||
os.makedirs(self.libs_dir, exist_ok=True)
|
||||
|
||||
# Add the library path to the modules, so they can be loaded from the plugin
|
||||
if self.libs_dir not in sys.path:
|
||||
sys.path.append(self.libs_dir)
|
||||
|
||||
# ----------------- Library Installation -----------------
|
||||
|
||||
def reset_current_library_installation(self):
|
||||
importlib.invalidate_caches()
|
||||
if self.libs_info_file.exists():
|
||||
print("Resetting current library installation, deleting library info file.")
|
||||
self.libs_info_file.unlink()
|
||||
try:
|
||||
shutil.rmtree(self.libs_dir)
|
||||
except PermissionError as e:
|
||||
print("Could not fully delete the library directory, please close Blender and try again.")
|
||||
print("Error:", e)
|
||||
|
||||
# Open Popup to tell the user it could not fully delete the library directory, please close Blender and try again.
|
||||
# def draw(self, context):
|
||||
# self.layout.label(text="Could not fully delete the library directory, please close Blender and try again.")
|
||||
#
|
||||
# bpy.context.window_manager.popup_menu(draw, title="Error", icon='ERROR')
|
||||
|
||||
|
||||
def install_libraries(self, required):
|
||||
missing_after_install = []
|
||||
missing_libs = []
|
||||
|
||||
# Install missing libraries
|
||||
missing = [mod for mod in required if not pkgutil.find_loader(mod)]
|
||||
if missing:
|
||||
# Ensure and update pip
|
||||
# Check which libs are missing
|
||||
for lib in required:
|
||||
lib_name = self._extract_lib_name(lib)
|
||||
print(f"Checking if {lib_name} exists: ", end='')
|
||||
if not pkgutil.find_loader(lib_name):
|
||||
print(f"not found")
|
||||
missing_libs.append(lib)
|
||||
else:
|
||||
print(f"found")
|
||||
|
||||
if missing_libs:
|
||||
self._update_pip()
|
||||
|
||||
# Install the missing libraries into the library path
|
||||
print("Installing missing libraries:", missing)
|
||||
print("Installing missing libraries:", missing_libs)
|
||||
try:
|
||||
# command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
|
||||
command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
|
||||
command = [
|
||||
self.python, '-m', 'pip', 'install',
|
||||
f"--target={str(self.libs_dir)}", *missing_libs
|
||||
]
|
||||
subprocess.check_call(command, stdout=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Installing libraries failed.")
|
||||
if self.os_name != "Windows":
|
||||
print("Retrying with sudo..")
|
||||
# command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
|
||||
command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
|
||||
command = ["sudo", self.python, '-m', 'pip', 'install',
|
||||
f"--target={str(self.libs_dir)}", *missing_libs]
|
||||
subprocess.call(command, stdout=subprocess.DEVNULL)
|
||||
finally:
|
||||
# Reset console color, because it could still be colored after running pip
|
||||
print('\033[39m')
|
||||
print('\033[39m') # reset console color
|
||||
|
||||
# Verify installation
|
||||
missing_after_install = [
|
||||
lib for lib in required
|
||||
if not pkgutil.find_loader(self._extract_lib_name(lib))
|
||||
]
|
||||
installed_libs = [lib for lib in missing_libs if lib not in missing_after_install]
|
||||
|
||||
# Check if all library installations were successful
|
||||
missing_after_install = [mod for mod in required if not pkgutil.find_loader(mod)]
|
||||
installed_libs = [lib for lib in missing if lib not in missing_after_install]
|
||||
if missing_after_install:
|
||||
print("WARNING: Could not install the following libraries:", missing_after_install)
|
||||
if installed_libs:
|
||||
print("Successfully installed missing libraries:", installed_libs)
|
||||
|
||||
# Create library info file after all libraries are installed to ensure everything is installed correctly
|
||||
print("Finished installing libraries")
|
||||
self.create_libs_info()
|
||||
|
||||
return missing_after_install
|
||||
|
||||
# ----------------- Library Info Handling -----------------
|
||||
|
||||
def check_libs_info(self):
|
||||
if not os.path.isdir(self.libs_dir):
|
||||
return
|
||||
|
||||
# If the library info file doesn't exist, delete the libs folder
|
||||
if not os.path.isfile(self.libs_info_file):
|
||||
print("Library info is missing, deleting library folder.")
|
||||
shutil.rmtree(self.libs_main_dir)
|
||||
return
|
||||
|
||||
# Read data from info file
|
||||
current_data = self.system_info
|
||||
with open(self.libs_info_file, 'r', encoding="utf8") as file:
|
||||
loaded_data = json.load(file)
|
||||
|
||||
# Compare info and delete libs folder if it doesn't match
|
||||
for key, val_current in current_data.items():
|
||||
val_loaded = loaded_data.get(key)
|
||||
if not val_loaded == val_current:
|
||||
if val_loaded != val_current:
|
||||
print("Current info:", current_data)
|
||||
print("Loaded info: ", loaded_data)
|
||||
print("Library info is not matching, deleting library folder.")
|
||||
@@ -112,14 +149,13 @@ class LibraryManager:
|
||||
return
|
||||
|
||||
def create_libs_info(self):
|
||||
# If the path doesn't exist or the info file already exists, don't create it
|
||||
if not os.path.isdir(self.libs_dir) or os.path.isfile(self.libs_info_file):
|
||||
return
|
||||
|
||||
# Write the current data to the info file
|
||||
with open(self.libs_info_file, 'w', encoding="utf8") as file:
|
||||
json.dump(self.system_info, file)
|
||||
|
||||
# ----------------- Pip Handling -----------------
|
||||
|
||||
def _update_pip(self):
|
||||
if self.pip_is_updated:
|
||||
return
|
||||
@@ -127,23 +163,19 @@ class LibraryManager:
|
||||
print("Ensuring pip")
|
||||
try:
|
||||
ensurepip.bootstrap()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Ensuring pip failed.")
|
||||
except Exception as e:
|
||||
print("Ensuring pip failed:", e)
|
||||
|
||||
print("Updating pip")
|
||||
try:
|
||||
# subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
|
||||
subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "pip"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Updating pip failed.")
|
||||
if self.os_name != "Windows":
|
||||
print("Retrying with sudo..")
|
||||
# subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
|
||||
subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "pip"])
|
||||
finally:
|
||||
# Reset console color, because it could still be colored after running pip
|
||||
print('\033[39m')
|
||||
|
||||
self.pip_is_updated = True
|
||||
|
||||
@@ -13,6 +13,7 @@ import traceback
|
||||
import webbrowser
|
||||
|
||||
from .. import updater
|
||||
from . import library_manager
|
||||
from .utils import ui_refresh_all, cancel_gen
|
||||
|
||||
from threading import Thread, Timer
|
||||
@@ -35,7 +36,14 @@ try:
|
||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
loaded_all_libs = True
|
||||
except ImportError as e:
|
||||
print(e)
|
||||
print(traceback.format_exc())
|
||||
library_manager.lib_manager.reset_current_library_installation()
|
||||
|
||||
# from .library_manager import lib_manager
|
||||
# lib_manager = lib_manager()
|
||||
#
|
||||
# # TODO: Maybe try importing the libraries
|
||||
|
||||
|
||||
|
||||
# Disable SSL
|
||||
|
||||
@@ -3,6 +3,7 @@ import copy
|
||||
from math import radians, degrees
|
||||
|
||||
from collections import OrderedDict
|
||||
from . import utils
|
||||
|
||||
recorded_data = {}
|
||||
recorded_timestamps = OrderedDict()
|
||||
@@ -132,7 +133,7 @@ def process_actor_recording(obj_name, data):
|
||||
index_len = len(values[0])
|
||||
|
||||
for axis_i in range(index_len):
|
||||
curve = action.fcurves.new(data_path=data_path, index=axis_i)
|
||||
curve = utils.create_fcurve_in_action(action, data_path, axis_i)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
@@ -179,7 +180,7 @@ def process_object_recording(obj_name, data):
|
||||
index_len = 3 if data_path.endswith('location') else 4
|
||||
|
||||
for axis_i in range(index_len):
|
||||
curve = action.fcurves.new(data_path=data_path, index=axis_i)
|
||||
curve = utils.create_fcurve_in_action(action, data_path, axis_i)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
@@ -216,7 +217,7 @@ def process_face_recording(obj_name, data):
|
||||
# print(data_path)
|
||||
frame_count = len(values)
|
||||
|
||||
curve = action.fcurves.new(data_path=data_path, index=0)
|
||||
curve = utils.create_fcurve_in_action(action, data_path, 0)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import bpy
|
||||
import math
|
||||
from mathutils import Vector, Matrix
|
||||
import sys
|
||||
|
||||
from bpy_extras import anim_utils
|
||||
from contextlib import suppress
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
def ui_refresh_properties():
|
||||
@@ -83,3 +87,56 @@ async def cancel_gen(agen):
|
||||
with suppress(Exception):
|
||||
await task
|
||||
await agen.aclose()
|
||||
|
||||
|
||||
def get_fcurves_from_action(action: bpy.types.Action, slot_identifier: int = 0) -> list[Any]:
|
||||
"""Returns all F-curves in the action that match the slot identifier.
|
||||
:param action: The action to search in
|
||||
:param slot_identifier: The slot identifier to search for. Only needed for Blender 5.0.0 and newer.
|
||||
:return: A list of F-curves that match the slot identifier
|
||||
"""
|
||||
if hasattr(action, 'fcurves'):
|
||||
fcurves = action.fcurves
|
||||
else: # Blender 5.0.0 and newer
|
||||
action_slot = action.slots[slot_identifier] # TODO: Add UI selection for action slot. Currently only uses the first slot.
|
||||
channelbag: bpy.types.ActionChannelbag = anim_utils.action_get_channelbag_for_slot(action, action_slot)
|
||||
fcurves = channelbag.fcurves if channelbag else []
|
||||
return fcurves
|
||||
|
||||
|
||||
def create_fcurve_in_action(action: bpy.types.Action, data_path: str, array_index: int = -1, slot_identifier: int = 0, action_group: str = "") -> bpy.types.FCurve:
|
||||
"""Creates a new F-curve in the action with the given data path and array index.
|
||||
:param action: The action to create the F-curve in
|
||||
:param data_path: The data path of the F-curve
|
||||
:param array_index: The array index of the F-curve
|
||||
:param slot_identifier: The slot identifier to create the F-curve in. Only needed for Blender 5.0.0 and newer.
|
||||
:param action_group: The action group to assign the F-curve to. Only needed for Blender 5.0.0 and newer.
|
||||
:return: The created F-curve
|
||||
"""
|
||||
# Blender 4.5 and older (Legacy)
|
||||
if hasattr(action, 'fcurves'):
|
||||
# Note: In 5.0, action.fcurves is removed. hasattr check ensures backward compatibility.
|
||||
fcurves = action.fcurves
|
||||
fcurve = fcurves.new(data_path=data_path, index=array_index, action_group=action_group)
|
||||
|
||||
# Blender 5.0+ (Slotted Actions)
|
||||
else:
|
||||
# 1. Ensure a slot exists
|
||||
if not action.slots:
|
||||
action.slots.new(name="Slot_0", id_type="OBJECT")
|
||||
|
||||
# 2. Get the Slot Object
|
||||
try:
|
||||
action_slot = action.slots[slot_identifier]
|
||||
except IndexError:
|
||||
# Fallback if the requested identifier doesn't exist
|
||||
action_slot = action.slots.new(name=f"Slot_{slot_identifier}", id_type="OBJECT")
|
||||
|
||||
# 3. Ensure the Channelbag exists
|
||||
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, action_slot)
|
||||
|
||||
# 4. Ensure/Create the F-Curve
|
||||
# Note: 'group_name' is the 5.0 parameter for 'action_group'
|
||||
fcurve = channelbag.fcurves.ensure(data_path, index=array_index, group_name=action_group)
|
||||
|
||||
return fcurve
|
||||
|
||||
Reference in New Issue
Block a user