Files
NexusLauncher/plugins/substancepainter/launcher.py
2025-11-23 20:41:50 +08:00

436 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()