This commit is contained in:
2025-11-23 20:41:50 +08:00
commit f7d5b7be07
65 changed files with 14986 additions and 0 deletions

2155
plugins/Qt.py Normal file

File diff suppressed because it is too large Load Diff

92
plugins/__init__.py Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
NexusLauncher 插件系统
提供 DCC 软件的插件启动功能
"""
import os
from typing import Optional, Dict
# 导入模块
from plugins.maya import launch_maya
from plugins.substancepainter import launch_substance_painter
from config.constants import APP_ICON_MAPPING
class PluginLauncher:
"""插件启动器 - 统一的启动接口"""
@staticmethod
def _get_app_type(app_name: str) -> Optional[str]:
"""通过 APP_ICON_MAPPING 识别应用类型
Args:
app_name: 应用名称
Returns:
应用类型 (Maya, SubstancePainter 等) 或 None
"""
app_name_lower = app_name.lower()
# 遍历映射表查找匹配
for key, app_type in APP_ICON_MAPPING.items():
if key in app_name_lower:
return app_type
return None
@staticmethod
def launch_with_plugins(app_name: str, app_path: str, plugin_config: Optional[Dict[str, str]] = None,
project_name: str = "NexusLauncher") -> bool:
"""根据应用类型启动应用并加载插件
Args:
app_name: 应用名称
app_path: 应用可执行文件路径
plugin_config: 插件配置字典,包含 maya_plugin_path, sp_shelf_path 等
project_name: 项目名称,用于 SP 库名称等
Returns:
是否成功启动
"""
if not plugin_config:
print(f"[INFO] No plugin config provided, launching {app_name} normally")
return False
# 通过 APP_ICON_MAPPING 识别应用类型
app_type = PluginLauncher._get_app_type(app_name)
if not app_type:
print(f"[INFO] Unknown app type for {app_name}, launching normally")
return False
print(f"[INFO] Detected app type: {app_type}")
# Maya
if app_type == "Maya":
maya_plugin_path = plugin_config.get('maya_plugin_path')
if maya_plugin_path:
print(f"[INFO] Launching Maya with plugins from: {maya_plugin_path}")
return launch_maya(app_path, maya_plugin_path)
else:
print(f"[WARNING] maya_plugin_path not found in config")
return False
# Substance Painter
elif app_type == "SubstancePainter":
sp_shelf_path = plugin_config.get('sp_shelf_path')
if sp_shelf_path:
print(f"[INFO] Launching Substance Painter with shelf: {sp_shelf_path}")
print(f"[INFO] Project: {project_name}")
return launch_substance_painter(app_path, sp_shelf_path, project_name)
else:
print(f"[WARNING] sp_shelf_path not found in config")
return False
# 不支持的应用类型
else:
print(f"[INFO] No plugin support for {app_type}, launching normally")
return False
__all__ = ['PluginLauncher', 'launch_maya', 'launch_substance_painter']

245
plugins/maya.py Normal file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Maya 插件启动模块
负责检测 Maya 版本并设置正确的插件路径
"""
import os
import re
import subprocess
from typing import Dict, Optional, Tuple, List
# 尝试导入 Qt使用 Qt.py 模块实现跨版本兼容
try:
from .Qt import QtWidgets
QMessageBox = QtWidgets.QMessageBox
HAS_QT = True
except ImportError:
HAS_QT = False
QMessageBox = None
class MayaLauncher:
"""Maya 启动器"""
def __init__(self, maya_exe_path: str, plugin_base_path: str):
"""
初始化 Maya 启动器
Args:
maya_exe_path: Maya 可执行文件路径
plugin_base_path: 插件基础路径(不含版本号)
"""
self.maya_exe_path = maya_exe_path
self.plugin_base_path = plugin_base_path
self.maya_version = self._detect_maya_version()
def _detect_maya_version(self) -> Optional[str]:
"""从 Maya 路径中检测版本号
支持的格式:
- Maya2023 -> 2023
- Maya2023.2 -> 2023
- Maya2025.0.1 -> 2025
Returns:
主版本号字符串,如 "2023", "2025" 等,如果检测失败返回 None
"""
# 从路径中提取版本号(支持 Maya2023, Maya2023.2, Maya2025.0.1 等格式)
match = re.search(r'Maya(\d{4})(?:\.\d+)*', self.maya_exe_path, re.IGNORECASE)
if match:
version = match.group(1) # 只取主版本号
print(f"[INFO] Detected Maya major version: {version}")
return version
print(f"[WARNING] Could not detect Maya version from path: {self.maya_exe_path}")
return None
def _get_available_plugin_versions(self) -> List[str]:
"""获取所有可用的插件版本
Returns:
可用版本列表,如 ['2023', '2024', '2025']
"""
if not os.path.exists(self.plugin_base_path):
return []
versions = []
try:
for item in os.listdir(self.plugin_base_path):
item_path = os.path.join(self.plugin_base_path, item)
# 检查是否是目录且名称是4位数字版本号
if os.path.isdir(item_path) and re.match(r'^\d{4}$', item):
versions.append(item)
except Exception as e:
print(f"[WARNING] Error scanning plugin versions: {e}")
return sorted(versions)
def _get_versioned_plugin_path(self) -> Tuple[Optional[str], Optional[str]]:
"""获取带版本号的插件路径
Returns:
(插件路径, 错误消息) 元组
- 如果成功: (路径, None)
- 如果失败: (None, 错误消息)
"""
if not self.maya_version:
return None, "无法检测 Maya 版本号"
# 拼接版本号路径
versioned_path = os.path.join(self.plugin_base_path, self.maya_version)
# 检查路径是否存在
if not os.path.exists(versioned_path):
# 获取所有可用版本
available_versions = self._get_available_plugin_versions()
if not available_versions:
error_msg = (
f"插件目录不存在:\n{self.plugin_base_path}\n\n"
f"未找到任何可用的插件版本。"
)
else:
error_msg = (
f"Maya 版本: {self.maya_version}\n"
f"插件版本不匹配!\n\n"
f"当前 Maya 版本: {self.maya_version}\n"
f"可用插件版本: {', '.join(available_versions)}\n\n"
f"插件将不会被加载。\n"
f"请检查 Maya 软件版本或安装对应版本的插件。"
)
return None, error_msg
print(f"[INFO] Using plugin path: {versioned_path}")
return versioned_path, None
def _setup_environment(self) -> Tuple[Dict[str, str], Optional[str]]:
"""设置 Maya 环境变量
Returns:
(环境变量字典, 错误消息) 元组
"""
env = os.environ.copy()
# 获取带版本号的插件路径
plugin_path, error_msg = self._get_versioned_plugin_path()
if not plugin_path:
return env, error_msg
# 设置 Maya 环境变量
shelves_path = os.path.join(plugin_path, "shelves")
scripts_path = os.path.join(plugin_path, "scripts")
plugins_path = os.path.join(plugin_path, "plug-ins")
icons_path = os.path.join(plugin_path, "icons")
# MAYA_SHELF_PATH - 工具架路径(追加到现有路径)
if os.path.exists(shelves_path):
existing_shelf_path = env.get("MAYA_SHELF_PATH", "")
if existing_shelf_path:
env["MAYA_SHELF_PATH"] = f"{shelves_path};{existing_shelf_path}"
else:
env["MAYA_SHELF_PATH"] = shelves_path
print(f"[OK] Set MAYA_SHELF_PATH: {shelves_path}")
# MAYA_SCRIPT_PATH - MEL/Python 脚本路径(追加到现有路径)
if os.path.exists(scripts_path):
existing_script_path = env.get("MAYA_SCRIPT_PATH", "")
if existing_script_path:
env["MAYA_SCRIPT_PATH"] = f"{scripts_path};{existing_script_path}"
else:
env["MAYA_SCRIPT_PATH"] = scripts_path
print(f"[OK] Set MAYA_SCRIPT_PATH: {scripts_path}")
# PYTHONPATH - Python 模块路径
if os.path.exists(scripts_path):
existing_pythonpath = env.get("PYTHONPATH", "")
if existing_pythonpath:
env["PYTHONPATH"] = f"{scripts_path};{existing_pythonpath}"
else:
env["PYTHONPATH"] = scripts_path
print(f"[OK] Set PYTHONPATH: {scripts_path}")
# MAYA_PLUG_IN_PATH - 插件路径(追加到现有路径)
if os.path.exists(plugins_path):
existing_plugin_path = env.get("MAYA_PLUG_IN_PATH", "")
if existing_plugin_path:
env["MAYA_PLUG_IN_PATH"] = f"{plugins_path};{existing_plugin_path}"
else:
env["MAYA_PLUG_IN_PATH"] = plugins_path
print(f"[OK] Set MAYA_PLUG_IN_PATH: {plugins_path}")
# XBMLANGPATH - 图标路径(追加到现有路径)
if os.path.exists(icons_path):
existing_icon_path = env.get("XBMLANGPATH", "")
if existing_icon_path:
env["XBMLANGPATH"] = f"{icons_path};{existing_icon_path}"
else:
env["XBMLANGPATH"] = icons_path
print(f"[OK] Set XBMLANGPATH: {icons_path}")
return env, None
def launch(self, show_error_dialog: bool = True) -> bool:
"""启动 Maya
Args:
show_error_dialog: 是否显示错误对话框
Returns:
是否成功启动
"""
try:
# 检查 Maya 可执行文件是否存在
if not os.path.exists(self.maya_exe_path):
error_msg = f"Maya 可执行文件不存在:\n{self.maya_exe_path}"
print(f"[ERROR] {error_msg}")
if show_error_dialog and HAS_QT:
QMessageBox.critical(None, "Maya 启动失败", error_msg)
return False
# 设置环境变量
env, error_msg = self._setup_environment()
# 如果有错误消息,显示警告但继续启动(不加载插件)
if error_msg:
print(f"[WARNING] {error_msg}")
if show_error_dialog and HAS_QT:
QMessageBox.warning(
None,
"插件版本不匹配",
error_msg + "\n\nMaya 将在不加载插件的情况下启动。"
)
# 启动 Maya
print(f"[INFO] Launching Maya: {self.maya_exe_path}")
subprocess.Popen([self.maya_exe_path], env=env)
if error_msg:
print(f"[OK] Maya launched without plugins")
else:
print(f"[OK] Maya launched successfully with plugins")
return True
except Exception as e:
error_msg = f"启动 Maya 时发生错误:\n{str(e)}"
print(f"[ERROR] {error_msg}")
if show_error_dialog and HAS_QT:
QMessageBox.critical(None, "Maya 启动失败", error_msg)
return False
def launch_maya(maya_exe_path: str, plugin_base_path: str) -> bool:
"""启动 Maya 的便捷函数
Args:
maya_exe_path: Maya 可执行文件路径
plugin_base_path: 插件基础路径(不含版本号)
Returns:
是否成功启动
"""
launcher = MayaLauncher(maya_exe_path, plugin_base_path)
return launcher.launch()

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Substance Painter 插件模块
"""
from .launcher import SubstancePainterLauncher, launch_substance_painter
__all__ = ['SubstancePainterLauncher', 'launch_substance_painter']

View File

@@ -0,0 +1,435 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Substance Painter 启动器
负责设置 SP 库路径和自动配置
"""
import os
import json
import subprocess
import shutil
import tempfile
import time
import psutil
from typing import Dict, Optional
from pathlib import Path
from .registry_manager import SPRegistryManager
class SubstancePainterLauncher:
"""Substance Painter 启动器"""
def __init__(self, sp_exe_path: str, shelf_path: str, project_name: str = "NexusLauncher"):
"""
初始化 Substance Painter 启动器
Args:
sp_exe_path: Substance Painter 可执行文件路径
shelf_path: 库路径sp_shelf_path
project_name: 项目名称,用作库名称
"""
self.sp_exe_path = sp_exe_path
self.shelf_path = shelf_path
self.project_name = project_name
# SP 配置文件路径
self.sp_config_dir = self._get_sp_config_dir()
def _get_sp_config_dir(self) -> Optional[Path]:
"""获取 Substance Painter 配置目录
Returns:
配置目录路径,如果找不到返回 None
"""
# 方法1: 使用 USERPROFILE
userprofile = os.environ.get('USERPROFILE', '')
if userprofile:
documents = Path(userprofile) / 'Documents'
else:
documents = Path.home() / 'Documents'
# 方法2: 使用注册表获取文档路径
try:
import winreg
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
documents_path, _ = winreg.QueryValueEx(key, "Personal")
winreg.CloseKey(key)
documents = Path(documents_path)
print(f"[SubstancePainter] Documents folder (from registry): {documents}")
except Exception as e:
print(f"[SubstancePainter] Could not get documents from registry: {e}")
# 尝试查找 Substance Painter 配置目录
sp_dirs = [
# SP 2023+
documents / 'Adobe' / 'Adobe Substance 3D Painter',
# 旧版本
documents / 'Allegorithmic' / 'Substance Painter',
# 备用路径
Path(userprofile) / 'AppData' / 'Local' / 'Adobe' / 'Adobe Substance 3D Painter',
Path(userprofile) / 'AppData' / 'Roaming' / 'Adobe' / 'Adobe Substance 3D Painter',
]
print(f"[SubstancePainter] Searching for config directory...")
for sp_dir in sp_dirs:
print(f"[SubstancePainter] Checking: {sp_dir}")
if sp_dir.exists():
print(f"[SubstancePainter] ✓ Found config directory: {sp_dir}")
# 列出配置目录中的文件
try:
config_files = list(sp_dir.glob('*.json'))
if config_files:
print(f"[SubstancePainter] Config files found:")
for f in config_files:
print(f"[SubstancePainter] - {f.name}")
except Exception as e:
print(f"[SubstancePainter] Could not list config files: {e}")
return sp_dir
# 如果找不到,尝试创建默认目录
default_dir = documents / 'Adobe' / 'Adobe Substance 3D Painter'
print(f"[SubstancePainter] Config directory not found")
print(f"[SubstancePainter] Will create default directory: {default_dir}")
try:
default_dir.mkdir(parents=True, exist_ok=True)
print(f"[SubstancePainter] ✓ Created config directory")
return default_dir
except Exception as e:
print(f"[SubstancePainter] ✗ Failed to create config directory: {e}")
return None
def _get_shelf_config_path(self) -> Optional[Path]:
"""获取库配置文件路径
Returns:
库配置文件路径,如果找不到返回 None
"""
if not self.sp_config_dir:
return None
# SP 库配置文件可能的位置
possible_configs = [
self.sp_config_dir / 'shelf.json',
self.sp_config_dir / 'shelves.json',
self.sp_config_dir / 'assets.json',
self.sp_config_dir / 'preferences.json',
]
# 先检查是否已存在配置文件
for config_path in possible_configs:
if config_path.exists():
print(f"[SubstancePainter] Found existing config: {config_path.name}")
return config_path
# 如果都不存在,使用默认的 shelf.json
shelf_config = self.sp_config_dir / 'shelf.json'
print(f"[SubstancePainter] Will create new config: {shelf_config.name}")
return shelf_config
def _create_default_shelf_config(self) -> dict:
"""创建默认的库配置
Returns:
默认库配置字典
"""
# SP 的库配置格式(基于官方文档)
# 库会自动创建标准文件夹结构:
# alphas, colorluts, effects, environments, generators, materials, etc.
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
import re
safe_name = self.project_name.lower().replace(" ", "_")
safe_name = re.sub(r'[^a-z0-9_-]', '', safe_name)
return {
"libraries": [
{
"name": safe_name,
"path": self.shelf_path.replace("\\", "/"),
"default": True
}
]
}
def _update_shelf_config(self) -> bool:
"""更新 Substance Painter 库配置
Returns:
是否成功更新
"""
try:
shelf_config_path = self._get_shelf_config_path()
if not shelf_config_path:
print(f"[SubstancePainter] Warning: Cannot update shelf config (config path not found)")
print(f"[SubstancePainter] SP will start with default library settings")
return False # 返回 False 但不影响启动
# 读取现有配置(如果存在)
if shelf_config_path.exists():
try:
with open(shelf_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
print(f"[SubstancePainter] Loaded existing config from: {shelf_config_path.name}")
except Exception as e:
print(f"[SubstancePainter] Failed to load existing config: {e}")
config = self._create_default_shelf_config()
else:
print(f"[SubstancePainter] Creating new config")
config = self._create_default_shelf_config()
# 确保有 libraries 列表SP 使用 "libraries" 而不是 "shelves"
if "libraries" not in config:
config["libraries"] = []
# 清理项目名称(移除空格和特殊字符,转为小写)
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
import re
clean_project_name = self.project_name.lower().replace(" ", "_")
clean_project_name = re.sub(r'[^a-z0-9_-]', '', clean_project_name)
# 查找是否已存在同名库
existing_library = None
for lib in config["libraries"]:
if lib.get("name") == clean_project_name:
existing_library = lib
break
if existing_library:
# 更新现有库
print(f"[SubstancePainter] Updating existing library: {clean_project_name}")
existing_library["path"] = self.shelf_path.replace("\\", "/")
existing_library["default"] = True
else:
# 添加新库
print(f"[SubstancePainter] Adding new library: {clean_project_name}")
config["libraries"].append({
"name": clean_project_name,
"path": self.shelf_path.replace("\\", "/"),
"default": True
})
# 将其他库设置为非默认
for lib in config["libraries"]:
if lib.get("name") != clean_project_name:
lib["default"] = False
# 保存配置
shelf_config_path.parent.mkdir(parents=True, exist_ok=True)
with open(shelf_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print(f"[SubstancePainter] ✓ Shelf config updated successfully")
print(f"[SubstancePainter] Library: {self.project_name}")
print(f"[SubstancePainter] Path: {self.shelf_path}")
print(f"[SubstancePainter] Default: True")
return True
except Exception as e:
print(f"[SubstancePainter] Error updating shelf config: {e}")
import traceback
traceback.print_exc()
return False
def _create_library_structure(self) -> bool:
"""创建 SP 库的标准文件夹结构
Returns:
是否成功创建
"""
try:
# SP 库的标准文件夹结构
standard_folders = [
'alphas', # Alpha 贴图
'brushes', # 笔刷预设
'colorluts', # 颜色查找表
'effects', # 滤镜/效果
'environments', # 环境贴图
'generators', # 生成器
'materials', # 材质
'particles', # 粒子预设
'presets', # 预设
'shaders', # 着色器
'smart-materials', # 智能材质
'smart-masks', # 智能蒙版
'textures', # 纹理
'export-presets', # 导出预设
]
# 确保使用绝对路径
base_path = Path(self.shelf_path).resolve()
print(f"[SubstancePainter] Library base path: {base_path}")
# 创建基础目录
if not base_path.exists():
print(f"[SubstancePainter] Creating library directory...")
base_path.mkdir(parents=True, exist_ok=True)
# 创建标准文件夹
created_folders = []
for folder in standard_folders:
folder_path = base_path / folder
if not folder_path.exists():
folder_path.mkdir(exist_ok=True)
created_folders.append(folder)
if created_folders:
print(f"[SubstancePainter] Created {len(created_folders)} standard folders")
print(f"[SubstancePainter] ✓ Library structure ready")
else:
print(f"[SubstancePainter] ✓ Library structure already exists")
return True
except Exception as e:
print(f"[SubstancePainter] Error creating library structure: {e}")
return False
def _setup_environment(self) -> Dict[str, str]:
"""设置 Substance Painter 环境变量
Returns:
包含环境变量的字典
"""
env = os.environ.copy()
# 创建库文件夹结构
self._create_library_structure()
# 设置环境变量供 SP Python 插件使用
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
import re
clean_project_name = self.project_name.lower().replace(" ", "_")
clean_project_name = re.sub(r'[^a-z0-9_-]', '', clean_project_name)
env["NEXUS_SP_LIBRARY_NAME"] = clean_project_name
env["NEXUS_SP_LIBRARY_PATH"] = self.shelf_path
print(f"[SubstancePainter] Set NEXUS_SP_LIBRARY_NAME: {clean_project_name}")
print(f"[SubstancePainter] Set NEXUS_SP_LIBRARY_PATH: {self.shelf_path}")
# 创建临时插件目录并复制插件
try:
# 创建临时目录
temp_plugin_dir = Path(tempfile.gettempdir()) / "NexusLauncher_SP_Plugins"
temp_plugin_dir.mkdir(exist_ok=True)
# 复制插件文件
plugin_source = Path(__file__).parent / "sp_api_plugin.py"
plugin_dest = temp_plugin_dir / "sp_api_plugin.py"
if plugin_source.exists():
shutil.copy2(plugin_source, plugin_dest)
print(f"[SubstancePainter] Copied plugin to: {plugin_dest}")
# 设置 SP 插件路径
env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = str(temp_plugin_dir)
print(f"[SubstancePainter] Set SUBSTANCE_PAINTER_PLUGINS_PATH: {temp_plugin_dir}")
else:
print(f"[SubstancePainter] Warning: Plugin source not found: {plugin_source}")
except Exception as e:
print(f"[SubstancePainter] Warning: Failed to setup plugin: {e}")
return env
def _is_sp_running(self) -> bool:
"""检查 SP 是否正在运行"""
for proc in psutil.process_iter(['name']):
try:
if 'Adobe Substance 3D Painter' in proc.info['name']:
return True
except:
pass
return False
def _kill_sp(self):
"""关闭所有 SP 进程"""
print(f"[SubstancePainter] Closing existing SP instances...")
for proc in psutil.process_iter(['name', 'pid']):
try:
if 'Adobe Substance 3D Painter' in proc.info['name']:
proc.kill()
print(f"[SubstancePainter] Killed process: {proc.info['pid']}")
except:
pass
time.sleep(2) # 等待进程完全关闭
def launch(self) -> bool:
"""启动 Substance Painter
Returns:
是否成功启动
"""
try:
# 检查 SP 可执行文件是否存在
if not os.path.exists(self.sp_exe_path):
print(f"[SubstancePainter] Error: Executable not found: {self.sp_exe_path}")
return False
print(f"[SubstancePainter] ========================================")
print(f"[SubstancePainter] Launching Substance Painter")
print(f"[SubstancePainter] ========================================")
print(f"[SubstancePainter] Project: {self.project_name}")
print(f"[SubstancePainter] Shelf Path: {self.shelf_path}")
print(f"")
# 步骤 1: 清理旧的项目库
print(f"[SubstancePainter] Step 1: Cleaning old project libraries...")
SPRegistryManager.remove_project_libraries()
print(f"")
# 步骤 2: 添加当前项目库
print(f"[SubstancePainter] Step 2: Adding current project library...")
self._create_library_structure() # 确保库文件夹存在
success = SPRegistryManager.add_project_library(self.project_name, self.shelf_path, set_as_default=True)
if not success:
print(f"[SubstancePainter] Warning: Failed to add library to registry")
print(f"")
# 步骤 3: 检查是否需要重启 SP
if self._is_sp_running():
print(f"[SubstancePainter] Step 3: SP is running, restarting to apply changes...")
self._kill_sp()
time.sleep(2)
else:
print(f"[SubstancePainter] Step 3: No restart needed")
print(f"")
# 步骤 4: 设置环境变量
env = self._setup_environment()
# 步骤 5: 启动 Substance Painter
print(f"[SubstancePainter] Step 4: Starting application...")
subprocess.Popen([self.sp_exe_path], env=env)
print(f"[SubstancePainter] ✓ Substance Painter launched successfully")
print(f"[SubstancePainter] ========================================")
return True
except Exception as e:
print(f"[SubstancePainter] Error: Failed to launch: {e}")
import traceback
traceback.print_exc()
return False
def launch_substance_painter(sp_exe_path: str, shelf_path: str, project_name: str = "NexusLauncher") -> bool:
"""启动 Substance Painter 的便捷函数
Args:
sp_exe_path: Substance Painter 可执行文件路径
shelf_path: 库路径sp_shelf_path
project_name: 项目名称,用作库名称
Returns:
是否成功启动
"""
launcher = SubstancePainterLauncher(sp_exe_path, shelf_path, project_name)
return launcher.launch()

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Substance Painter 注册表管理器
用于在启动前配置库,关闭后清理库
"""
import winreg
import re
import os
class SPRegistryManager:
"""SP 注册表管理器"""
REGISTRY_KEY = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf\pathInfos"
SHELF_KEY = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf"
@staticmethod
def normalize_library_name(name):
"""规范化库名称(小写、下划线、连字符)"""
safe_name = name.lower().replace(' ', '_')
safe_name = re.sub(r'[^a-z0-9_-]', '', safe_name)
return safe_name
@staticmethod
def get_all_libraries():
"""获取所有库配置
Returns:
list: [(id, name, path, disabled), ...]
"""
try:
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
key = winreg.OpenKey(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_READ)
libraries = []
sub_key_count = winreg.QueryInfoKey(key)[0]
for i in range(sub_key_count):
sub_key_name = winreg.EnumKey(key, i)
sub_key = winreg.OpenKey(reg_conn, f"{SPRegistryManager.REGISTRY_KEY}\\{sub_key_name}", 0, winreg.KEY_READ)
try:
name = winreg.QueryValueEx(sub_key, "name")[0]
path = winreg.QueryValueEx(sub_key, "path")[0]
disabled = winreg.QueryValueEx(sub_key, "disabled")[0]
libraries.append((int(sub_key_name), name, path, disabled))
except:
pass
finally:
winreg.CloseKey(sub_key)
winreg.CloseKey(key)
return libraries
except Exception as e:
print(f"[Registry] Error reading libraries: {e}")
return []
@staticmethod
def remove_project_libraries():
"""删除所有项目库(保留系统库)
系统库your_assets, starter_assets, system_fonts, user_fonts
"""
system_libs = ['your_assets', 'starter_assets', 'system_fonts', 'user_fonts']
try:
libraries = SPRegistryManager.get_all_libraries()
to_remove = []
for lib_id, name, path, disabled in libraries:
if name not in system_libs:
to_remove.append(lib_id)
print(f"[Registry] Will remove: {name} (ID: {lib_id})")
if not to_remove:
print(f"[Registry] No project libraries to remove")
return True
# 删除项目库
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
for lib_id in to_remove:
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id}")
print(f"[Registry] Removed library ID: {lib_id}")
except Exception as e:
print(f"[Registry] Error removing {lib_id}: {e}")
# 重新编号剩余的库(确保连续)
SPRegistryManager._reindex_libraries()
return True
except Exception as e:
print(f"[Registry] Error removing libraries: {e}")
return False
@staticmethod
def _reindex_libraries():
"""重新编号库,确保 ID 连续"""
try:
libraries = SPRegistryManager.get_all_libraries()
libraries.sort(key=lambda x: x[0]) # 按 ID 排序
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
# 如果已经连续,不需要重新编号
expected_ids = list(range(1, len(libraries) + 1))
actual_ids = [lib[0] for lib in libraries]
if expected_ids == actual_ids:
print(f"[Registry] Library IDs are already continuous")
return
print(f"[Registry] Reindexing libraries...")
# 先将所有库移到临时 ID
temp_offset = 1000
for lib_id, name, path, disabled in libraries:
old_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id}"
new_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{lib_id + temp_offset}"
# 读取旧值
old_key = winreg.OpenKey(reg_conn, old_key_path, 0, winreg.KEY_READ)
name_val = winreg.QueryValueEx(old_key, "name")[0]
path_val = winreg.QueryValueEx(old_key, "path")[0]
disabled_val = winreg.QueryValueEx(old_key, "disabled")[0]
winreg.CloseKey(old_key)
# 创建新键
new_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, new_key_path)
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, name_val)
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, path_val)
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, disabled_val)
winreg.CloseKey(new_key)
# 删除旧键
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, old_key_path)
# 再将临时 ID 移到正确的连续 ID
for new_id, (old_id, name, path, disabled) in enumerate(libraries, start=1):
temp_id = old_id + temp_offset
temp_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{temp_id}"
final_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{new_id}"
# 读取临时键
temp_key = winreg.OpenKey(reg_conn, temp_key_path, 0, winreg.KEY_READ)
name_val = winreg.QueryValueEx(temp_key, "name")[0]
path_val = winreg.QueryValueEx(temp_key, "path")[0]
disabled_val = winreg.QueryValueEx(temp_key, "disabled")[0]
winreg.CloseKey(temp_key)
# 创建最终键
final_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, final_key_path)
winreg.SetValueEx(final_key, "name", 0, winreg.REG_SZ, name_val)
winreg.SetValueEx(final_key, "path", 0, winreg.REG_SZ, path_val)
winreg.SetValueEx(final_key, "disabled", 0, winreg.REG_SZ, disabled_val)
winreg.CloseKey(final_key)
# 删除临时键
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, temp_key_path)
# 更新 size
key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, len(libraries))
winreg.CloseKey(key)
print(f"[Registry] Reindexing complete, new size: {len(libraries)}")
except Exception as e:
print(f"[Registry] Error reindexing: {e}")
import traceback
traceback.print_exc()
@staticmethod
def add_project_library(library_name, library_path, set_as_default=True):
"""添加项目库
Args:
library_name: 库名称
library_path: 库路径
set_as_default: 是否设置为默认
Returns:
bool: 是否成功
"""
try:
safe_name = SPRegistryManager.normalize_library_name(library_name)
normalized_path = library_path.replace('\\', '/')
print(f"[Registry] Adding library: {safe_name}")
print(f"[Registry] Path: {normalized_path}")
# 检查是否已存在
libraries = SPRegistryManager.get_all_libraries()
for lib_id, name, path, disabled in libraries:
if name == safe_name:
print(f"[Registry] Library already exists: {name}")
return True
# 找到下一个 ID
if libraries:
next_id = max(lib[0] for lib in libraries) + 1
else:
next_id = 1
# 创建新库
reg_conn = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
new_key_path = f"{SPRegistryManager.REGISTRY_KEY}\\{next_id}"
new_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, new_key_path)
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, safe_name)
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, normalized_path)
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, "false")
winreg.CloseKey(new_key)
# 更新 size
key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.REGISTRY_KEY, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, len(libraries) + 1)
winreg.CloseKey(key)
print(f"[Registry] Library added with ID: {next_id}")
# 设置为默认库
if set_as_default:
shelf_key = winreg.OpenKeyEx(reg_conn, SPRegistryManager.SHELF_KEY, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(shelf_key, "writableShelf", 0, winreg.REG_SZ, safe_name)
winreg.CloseKey(shelf_key)
print(f"[Registry] Set as default library")
return True
except Exception as e:
print(f"[Registry] Error adding library: {e}")
import traceback
traceback.print_exc()
return False
@staticmethod
def needs_restart():
"""检查是否需要重启 SP 以应用更改
Returns:
bool: 是否需要重启
"""
# 如果 SP 正在运行,注册表更改需要重启才能生效
import psutil
for proc in psutil.process_iter(['name']):
if 'Adobe Substance 3D Painter' in proc.info['name']:
return True
return False

View File

@@ -0,0 +1,318 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Substance Painter API 插件
通过 Python API 自动添加项目库
此文件会被复制到临时目录,在 SP 启动时自动执行
"""
import os
import sys
def setup_project_library_via_registry(library_name, library_path):
"""通过注册表设置项目库Windows
Args:
library_name: 库名称
library_path: 库路径
Returns:
bool: 是否成功
"""
try:
import winreg
import re
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
safe_library_name = library_name.lower().replace(' ', '_')
safe_library_name = re.sub(r'[^a-z0-9_-]', '', safe_library_name)
print(f"[NexusLauncher] Setting up library via registry: {library_name}")
if safe_library_name != library_name:
print(f"[NexusLauncher] Normalized name: {safe_library_name}")
print(f"[NexusLauncher] Path: {library_path}")
# 检查路径是否存在
if not os.path.exists(library_path):
print(f"[NexusLauncher] Error: Library path does not exist: {library_path}")
return False
# 规范化路径
normalized_path = library_path.replace('\\', '/')
# 注册表路径
registry_key_name = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf\pathInfos"
# 连接到注册表
reg_connection = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
try:
# 打开父键
key = winreg.OpenKey(reg_connection, registry_key_name, 0, winreg.KEY_READ)
except FileNotFoundError:
print(f"[NexusLauncher] Registry key not found. Please open SP settings once to create it.")
return False
# 检查是否已存在同名库
sub_key_count = winreg.QueryInfoKey(key)[0]
shelf_exists = False
shelf_number = 0
for x in range(sub_key_count):
sub_key_name = winreg.EnumKey(key, x)
shelf_number = max(shelf_number, int(sub_key_name))
sub_key = winreg.OpenKey(reg_connection, registry_key_name + "\\" + sub_key_name, 0, winreg.KEY_READ)
try:
existing_name = winreg.QueryValueEx(sub_key, "name")[0]
existing_path = winreg.QueryValueEx(sub_key, "path")[0]
if existing_name == safe_library_name:
print(f"[NexusLauncher] Library already exists: {existing_name} at {existing_path}")
shelf_exists = True
winreg.CloseKey(sub_key)
break
finally:
winreg.CloseKey(sub_key)
if not shelf_exists:
# 添加新库
shelf_number += 1
print(f"[NexusLauncher] Adding new library with ID: {shelf_number}")
# 创建新键
new_key = winreg.CreateKey(key, str(shelf_number))
winreg.SetValueEx(new_key, "disabled", 0, winreg.REG_SZ, "false")
winreg.SetValueEx(new_key, "name", 0, winreg.REG_SZ, safe_library_name)
winreg.SetValueEx(new_key, "path", 0, winreg.REG_SZ, normalized_path)
winreg.CloseKey(new_key)
# 更新计数
try:
count = winreg.QueryValueEx(key, "size")[0]
new_count = count + 1
except:
# 如果读取失败,使用当前最大的 shelf_number + 1
new_count = shelf_number + 1
winreg.CloseKey(key)
key = winreg.OpenKeyEx(reg_connection, registry_key_name, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(key, "size", 0, winreg.REG_DWORD, new_count)
print(f"[NexusLauncher] Updated size to: {new_count}")
print(f"[NexusLauncher] ✓ Library added to registry successfully")
# 设置为默认库writableShelf
try:
shelf_key = r"SOFTWARE\Adobe\Adobe Substance 3D Painter\Shelf"
default_key = winreg.OpenKeyEx(reg_connection, shelf_key, 0, winreg.KEY_SET_VALUE)
winreg.SetValueEx(default_key, "writableShelf", 0, winreg.REG_SZ, safe_library_name)
winreg.CloseKey(default_key)
print(f"[NexusLauncher] ✓ Set as default library (writableShelf)")
except Exception as e:
print(f"[NexusLauncher] Warning: Could not set as default: {e}")
winreg.CloseKey(key)
else:
winreg.CloseKey(key)
print(f"[NexusLauncher] ========================================")
print(f"[NexusLauncher] ✓ Library setup complete")
print(f"[NexusLauncher] Note: Restart SP to see the changes")
print(f"[NexusLauncher] ========================================")
return True
except Exception as e:
print(f"[NexusLauncher] Error: {e}")
import traceback
traceback.print_exc()
return False
def setup_project_library(library_name, library_path, set_as_default=True):
"""设置项目库
Args:
library_name: 库名称
library_path: 库路径
set_as_default: 是否设置为默认库
Returns:
bool: 是否成功
"""
try:
# 延迟导入,因为只有在 SP 中才能导入
import substance_painter.resource
# SP 库名称规则:只能包含小写字母、数字、下划线和连字符
# 将名称转换为小写并替换无效字符
safe_library_name = library_name.lower().replace(' ', '_')
# 移除其他无效字符
import re
safe_library_name = re.sub(r'[^a-z0-9_-]', '', safe_library_name)
print(f"[NexusLauncher] Setting up library: {library_name}")
if safe_library_name != library_name:
print(f"[NexusLauncher] Normalized name: {safe_library_name} (SP naming rules)")
print(f"[NexusLauncher] Path: {library_path}")
# 检查路径是否存在
if not os.path.exists(library_path):
print(f"[NexusLauncher] Error: Library path does not exist: {library_path}")
return False
# 获取所有现有的 Shelf
existing_shelves = substance_painter.resource.Shelves.all()
print(f"[NexusLauncher] Found {len(existing_shelves)} existing shelves")
# 检查是否已存在同名或同路径的库
shelf_exists = False
for shelf in existing_shelves:
shelf_name = shelf.name()
shelf_path = shelf.path()
print(f"[NexusLauncher] Existing shelf: {shelf_name} -> {shelf_path}")
# 规范化路径进行比较
from os.path import normpath
normalized_shelf_path = normpath(shelf_path).lower()
normalized_library_path = normpath(library_path).lower()
if shelf_name == safe_library_name or normalized_shelf_path == normalized_library_path:
print(f"[NexusLauncher] Library already exists: {shelf_name} at {shelf_path}")
shelf_exists = True
existing_shelf = shelf
break
if not shelf_exists:
# 添加新库
print(f"[NexusLauncher] Adding new library...")
try:
# 探索可用的 API 方法
print(f"[NexusLauncher] Available Shelf methods: {[m for m in dir(substance_painter.resource.Shelf) if not m.startswith('_')]}")
print(f"[NexusLauncher] Available Shelves methods: {[m for m in dir(substance_painter.resource.Shelves) if not m.startswith('_')]}")
# 检查是否有项目打开(添加库需要关闭项目)
try:
import substance_painter.project
if substance_painter.project.is_open():
print(f"[NexusLauncher] ERROR: A project is currently open!")
print(f"[NexusLauncher] According to SP API docs, no project should be open when adding shelves")
print(f"[NexusLauncher] Please close the project and try again")
print(f"[NexusLauncher] Or add the library manually: Edit -> Settings -> Libraries")
return False
else:
print(f"[NexusLauncher] ✓ No project is open, safe to add shelf")
except Exception as e:
print(f"[NexusLauncher] Warning: Could not check project status: {e}")
# 规范化路径为正斜杠格式SP 可能需要这个)
normalized_path = library_path.replace('\\', '/')
print(f"[NexusLauncher] Normalized path: {normalized_path}")
# 使用 Shelves.add 方法(使用规范化的名称)
print(f"[NexusLauncher] Calling Shelves.add('{safe_library_name}', '{normalized_path}')")
new_shelf = substance_painter.resource.Shelves.add(safe_library_name, normalized_path)
print(f"[NexusLauncher] ✓ Library added successfully")
# 刷新资源
substance_painter.resource.Shelves.refresh_all()
print(f"[NexusLauncher] ✓ Resources refreshed")
except Exception as e:
print(f"[NexusLauncher] Error adding library: {e}")
import traceback
traceback.print_exc()
return False
else:
print(f"[NexusLauncher] Library already configured")
# 设置为默认库(如果需要)
if set_as_default:
# 注意SP API 可能没有直接设置默认库的方法
# 这需要通过修改配置文件来实现
print(f"[NexusLauncher] Note: Default library setting may require manual configuration")
print(f"[NexusLauncher] ========================================")
print(f"[NexusLauncher] ✓ Library setup complete")
print(f"[NexusLauncher] ========================================")
return True
except ImportError as e:
print(f"[NexusLauncher] Error: Cannot import substance_painter module")
print(f"[NexusLauncher] This script must be run inside Substance Painter")
return False
except Exception as e:
print(f"[NexusLauncher] Error: {e}")
import traceback
traceback.print_exc()
return False
def get_library_info_from_env():
"""从环境变量获取库信息
Returns:
tuple: (library_name, library_path) 或 (None, None)
"""
# NexusLauncher 会设置这些环境变量
library_name = os.environ.get('NEXUS_SP_LIBRARY_NAME')
library_path = os.environ.get('NEXUS_SP_LIBRARY_PATH')
if library_name and library_path:
return library_name, library_path
return None, None
def main():
"""主函数"""
print(f"[NexusLauncher] ========================================")
print(f"[NexusLauncher] Substance Painter Library Setup Plugin")
print(f"[NexusLauncher] ========================================")
print(f"[NexusLauncher] Plugin loaded from: {__file__}")
print(f"[NexusLauncher] Python version: {sys.version}")
# 检查环境变量
print(f"[NexusLauncher] Checking environment variables...")
library_name = os.environ.get('NEXUS_SP_LIBRARY_NAME')
library_path = os.environ.get('NEXUS_SP_LIBRARY_PATH')
print(f"[NexusLauncher] NEXUS_SP_LIBRARY_NAME: {library_name}")
print(f"[NexusLauncher] NEXUS_SP_LIBRARY_PATH: {library_path}")
if not library_name or not library_path:
print(f"[NexusLauncher] Error: Library info not found in environment variables")
print(f"[NexusLauncher] Please launch SP through NexusLauncher")
return False
# 优先使用注册表方法(更可靠)
print(f"[NexusLauncher] Method 1: Using Windows Registry...")
success = setup_project_library_via_registry(library_name, library_path)
if not success:
print(f"[NexusLauncher] Method 2: Using Python API...")
success = setup_project_library(library_name, library_path, set_as_default=True)
return success
# SP 插件入口点
def start_plugin():
"""SP 插件启动入口点"""
main()
def close_plugin():
"""SP 插件关闭入口点"""
pass
# 如果作为插件运行
if __name__ == "__plugin__":
start_plugin()
# 如果作为脚本运行
elif __name__ == "__main__":
main()