Update
This commit is contained in:
2155
plugins/Qt.py
Normal file
2155
plugins/Qt.py
Normal file
File diff suppressed because it is too large
Load Diff
92
plugins/__init__.py
Normal file
92
plugins/__init__.py
Normal 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
245
plugins/maya.py
Normal 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()
|
||||
9
plugins/substancepainter/__init__.py
Normal file
9
plugins/substancepainter/__init__.py
Normal 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']
|
||||
435
plugins/substancepainter/launcher.py
Normal file
435
plugins/substancepainter/launcher.py
Normal 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()
|
||||
252
plugins/substancepainter/registry_manager.py
Normal file
252
plugins/substancepainter/registry_manager.py
Normal 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
|
||||
318
plugins/substancepainter/sp_api_plugin.py
Normal file
318
plugins/substancepainter/sp_api_plugin.py
Normal 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()
|
||||
Reference in New Issue
Block a user