#!/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()