dcc-bridge 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dcc_bridge/__init__.py +22 -0
- dcc_bridge/__main__.py +10 -0
- dcc_bridge/adapters/__init__.py +27 -0
- dcc_bridge/adapters/base.py +126 -0
- dcc_bridge/adapters/designer.py +99 -0
- dcc_bridge/adapters/max.py +84 -0
- dcc_bridge/adapters/maya.py +79 -0
- dcc_bridge/adapters/painter.py +82 -0
- dcc_bridge/cli/__init__.py +10 -0
- dcc_bridge/cli/cleanup.py +114 -0
- dcc_bridge/cli/main.py +45 -0
- dcc_bridge/cli/run.py +132 -0
- dcc_bridge/cli/setup.py +63 -0
- dcc_bridge/cli/status.py +102 -0
- dcc_bridge/cli/utils.py +63 -0
- dcc_bridge/client.py +196 -0
- dcc_bridge/dcc_types.py +54 -0
- dcc_bridge/debug.py +117 -0
- dcc_bridge/discovery.py +225 -0
- dcc_bridge/execute.py +190 -0
- dcc_bridge/protocol.py +155 -0
- dcc_bridge/reload.py +41 -0
- dcc_bridge/server.py +407 -0
- dcc_bridge/setup/__init__.py +8 -0
- dcc_bridge/setup/base.py +373 -0
- dcc_bridge/setup/designer.py +113 -0
- dcc_bridge/setup/max.py +90 -0
- dcc_bridge/setup/maya.py +140 -0
- dcc_bridge/setup/painter.py +113 -0
- dcc_bridge/start.py +214 -0
- dcc_bridge-1.0.0.dist-info/METADATA +445 -0
- dcc_bridge-1.0.0.dist-info/RECORD +34 -0
- dcc_bridge-1.0.0.dist-info/WHEEL +4 -0
- dcc_bridge-1.0.0.dist-info/entry_points.txt +2 -0
dcc_bridge/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dcc-bridge: DCC Python 桥接核心包
|
|
3
|
+
|
|
4
|
+
提供 DCC 端 TCP 服务端、通用 TCP 客户端、CLI 入口、DCC 适配器、
|
|
5
|
+
代码执行、模块热重载、debugpy 调试集成等底层能力。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1"
|
|
11
|
+
|
|
12
|
+
from .client import DCCClient
|
|
13
|
+
from .discovery import list_instances, get_instance, register_instance, unregister_instance
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"DCCClient",
|
|
18
|
+
"list_instances",
|
|
19
|
+
"get_instance",
|
|
20
|
+
"register_instance",
|
|
21
|
+
"unregister_instance",
|
|
22
|
+
]
|
dcc_bridge/__main__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DCC 适配器包
|
|
3
|
+
|
|
4
|
+
包含各 DCC 软件的适配器实现。当前实现:
|
|
5
|
+
- maya: MayaAdapter
|
|
6
|
+
- max: MaxAdapter
|
|
7
|
+
- painter: SubstancePainterAdapter(接口保留)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .base import DCCAdapter, Logger
|
|
13
|
+
from .maya import MayaAdapter
|
|
14
|
+
from .max import MaxAdapter
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .painter import SubstancePainterAdapter
|
|
18
|
+
except ImportError:
|
|
19
|
+
SubstancePainterAdapter = None # type: ignore
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DCCAdapter",
|
|
23
|
+
"Logger",
|
|
24
|
+
"MayaAdapter",
|
|
25
|
+
"MaxAdapter",
|
|
26
|
+
"SubstancePainterAdapter",
|
|
27
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DCC Adapter 基类
|
|
3
|
+
所有 DCC Adapter 都应继承此类并实现特定逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Logger:
|
|
14
|
+
"""通用日志类,各 DCC 可继承并覆盖"""
|
|
15
|
+
|
|
16
|
+
channel = "DCC Bridge"
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def info(cls, message: str):
|
|
20
|
+
print(f"[INFO] {cls.channel}: {message}")
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def warn(cls, message: str):
|
|
24
|
+
print(f"[WARN] {cls.channel}: {message}")
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def error(cls, message: str):
|
|
28
|
+
print(f"[ERROR] {cls.channel}: {message}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DCCAdapter:
|
|
32
|
+
"""
|
|
33
|
+
DCC Adapter 基类
|
|
34
|
+
|
|
35
|
+
各 DCC 应继承此类并实现特定逻辑。
|
|
36
|
+
基类提供默认实现,确保服务端可以正常运行。
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str = "unknown"
|
|
40
|
+
"""DCC 名称,如 "maya", "3dsmax", "substance_painter" """
|
|
41
|
+
|
|
42
|
+
def get_logger(self) -> Logger:
|
|
43
|
+
"""返回 DCC 特定的日志对象"""
|
|
44
|
+
return Logger
|
|
45
|
+
|
|
46
|
+
def get_python_path(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
返回 DCC 的 Python 解释器路径
|
|
49
|
+
|
|
50
|
+
用于 debugpy 配置和 pip 安装。
|
|
51
|
+
默认返回 sys.executable,各 DCC 子类可覆盖此方法。
|
|
52
|
+
"""
|
|
53
|
+
return sys.executable
|
|
54
|
+
|
|
55
|
+
def _on_connected(self, parent_window) -> None:
|
|
56
|
+
"""
|
|
57
|
+
连接建立后的初始化回调
|
|
58
|
+
|
|
59
|
+
可用于执行 DCC 特定的初始化操作
|
|
60
|
+
"""
|
|
61
|
+
logger = self.get_logger()
|
|
62
|
+
logger.info("DCC Bridge Server connected")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
try:
|
|
66
|
+
from PySide2.QtWidgets import QMessageBox
|
|
67
|
+
except ImportError:
|
|
68
|
+
from PySide6.QtWidgets import QMessageBox
|
|
69
|
+
QMessageBox.information(parent_window, "DCC Python", "DCC Bridge Server connected")
|
|
70
|
+
|
|
71
|
+
except:
|
|
72
|
+
logger.warn("PySide2/PySide6 not found, cannot show connection message box")
|
|
73
|
+
|
|
74
|
+
def add_sys_path(self, path: str) -> None:
|
|
75
|
+
"""
|
|
76
|
+
将指定路径添加到 sys.path
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
path: 要添加的路径
|
|
80
|
+
"""
|
|
81
|
+
if path not in sys.path:
|
|
82
|
+
sys.path.append(path)
|
|
83
|
+
|
|
84
|
+
def format_output(self, line: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
可选:DCC 特定的输出格式化
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
line: 原始输出行
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
格式化后的输出行
|
|
93
|
+
"""
|
|
94
|
+
return line
|
|
95
|
+
|
|
96
|
+
def is_main_thread(self) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
检查当前是否在主线程中
|
|
99
|
+
|
|
100
|
+
默认返回 True(假设调用者已确保在主线程)
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True 如果在主线程,否则 False
|
|
104
|
+
"""
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def ensure_main_thread(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
确保在主线程中执行(如果需要)
|
|
110
|
+
|
|
111
|
+
默认实现不做任何操作
|
|
112
|
+
"""
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def configure_debugpy(self, python_path: str) -> None:
|
|
116
|
+
"""
|
|
117
|
+
配置 debugpy 的全局选项
|
|
118
|
+
|
|
119
|
+
默认调用 debugpy.configure(python=python_path)。
|
|
120
|
+
各 DCC 子类可覆盖此方法来避免特定 DCC 下的兼容性问题。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
python_path: 用于启动子进程的 Python 解释器路径
|
|
124
|
+
"""
|
|
125
|
+
import debugpy
|
|
126
|
+
debugpy.configure(python=python_path)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SubstanceDesigner Adapter
|
|
3
|
+
实现 SubstanceDesigner 特定的日志、Python 路径和初始化逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .base import DCCAdapter, Logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SubstanceDesignerLogger(Logger):
|
|
15
|
+
"""SubstanceDesigner 专用日志类"""
|
|
16
|
+
|
|
17
|
+
# channel = "SubstanceDesigner"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def info(cls, message: str):
|
|
21
|
+
super().info(message)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def warn(cls, message: str):
|
|
25
|
+
super().warn(message)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def error(cls, message: str):
|
|
29
|
+
super().error(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SubstanceDesignerAdapter(DCCAdapter):
|
|
33
|
+
"""
|
|
34
|
+
SubstanceDesigner Adapter
|
|
35
|
+
|
|
36
|
+
实现 SubstanceDesigner 特定的逻辑,包括:
|
|
37
|
+
- 日志输出到 SubstanceDesigner 脚本编辑器
|
|
38
|
+
- Python 路径获取(python.exe)
|
|
39
|
+
- 初始化逻辑
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str = "substance_designer"
|
|
43
|
+
|
|
44
|
+
def get_logger(self) -> Logger:
|
|
45
|
+
return SubstanceDesignerLogger
|
|
46
|
+
|
|
47
|
+
def get_python_path(self) -> str:
|
|
48
|
+
"""
|
|
49
|
+
返回 SubstanceDesigner 的 Python 解释器路径(python.exe)
|
|
50
|
+
|
|
51
|
+
所有版本的SP的python解析器路径都位于根目录下的: "./plugins/pythonsdk/python.exe"
|
|
52
|
+
"""
|
|
53
|
+
exe_dir = os.path.dirname(super().get_python_path())
|
|
54
|
+
exe_name = "python.exe" if sys.platform == "win32" else "python"
|
|
55
|
+
return os.path.join(exe_dir, "plugins", "pythonsdk", exe_name)
|
|
56
|
+
|
|
57
|
+
def on_connected(self) -> None:
|
|
58
|
+
try:
|
|
59
|
+
import sd
|
|
60
|
+
ctx = sd.getContext()
|
|
61
|
+
app = ctx.getSDApplication()
|
|
62
|
+
uimgr = app.getUIMgr()
|
|
63
|
+
|
|
64
|
+
# 兼容Pyside2/Pyside6 + 对应shiboken
|
|
65
|
+
try:
|
|
66
|
+
from PySide2.QtWidgets import QWidget
|
|
67
|
+
from shiboken2 import wrapInstance
|
|
68
|
+
except ImportError:
|
|
69
|
+
from PySide6.QtWidgets import QWidget
|
|
70
|
+
from shiboken6 import wrapInstance
|
|
71
|
+
|
|
72
|
+
ptr = uimgr.getMainWindowPtr()
|
|
73
|
+
parent_window = wrapInstance(int(ptr), QWidget)
|
|
74
|
+
except:
|
|
75
|
+
logger = self.get_logger()
|
|
76
|
+
logger.error("无法获取主窗口。substance_designer.ui 不可用。")
|
|
77
|
+
return
|
|
78
|
+
super()._on_connected(parent_window)
|
|
79
|
+
|
|
80
|
+
def add_sys_path(self, path: str) -> None:
|
|
81
|
+
super().add_sys_path(path)
|
|
82
|
+
logger = self.get_logger()
|
|
83
|
+
logger.info(f"Added to {self.name} sys.path: {path}")
|
|
84
|
+
|
|
85
|
+
def configure_debugpy(self, python_path: str) -> None:
|
|
86
|
+
"""
|
|
87
|
+
SD 内置 Python 是打包冻结版(frozen modules),
|
|
88
|
+
debugpy 默认校验源码文件一致性,冻结内置库会触发断点失效警告,
|
|
89
|
+
两种解决思路:关闭冻结模块 / 跳过文件校验。
|
|
90
|
+
|
|
91
|
+
这里选择在脚本最顶部添加环境变量,提前关闭校验,从而屏蔽警告
|
|
92
|
+
Args:
|
|
93
|
+
python_path: 用于启动子进程的 Python 解释器路径
|
|
94
|
+
"""
|
|
95
|
+
# 放在所有import最前面
|
|
96
|
+
import os
|
|
97
|
+
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
|
|
98
|
+
import debugpy
|
|
99
|
+
debugpy.configure(python=python_path)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
3ds Max Adapter
|
|
3
|
+
实现 3ds Max 特定的日志、Python 路径和初始化逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .base import DCCAdapter, Logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaxLogger(Logger):
|
|
15
|
+
"""3ds Max 专用日志类"""
|
|
16
|
+
|
|
17
|
+
# channel = "3ds Max"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def info(cls, message: str):
|
|
21
|
+
super().info(message)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def warn(cls, message: str):
|
|
25
|
+
super().warn(message)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def error(cls, message: str):
|
|
29
|
+
super().error(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MaxAdapter(DCCAdapter):
|
|
33
|
+
"""
|
|
34
|
+
3ds Max Adapter
|
|
35
|
+
|
|
36
|
+
实现 3ds Max 特定的逻辑,包括:
|
|
37
|
+
- 日志输出到 3ds Max 脚本监听器
|
|
38
|
+
- Python 路径获取(3ds Max 自带的 Python)
|
|
39
|
+
- 初始化逻辑
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str = "3dsmax"
|
|
43
|
+
|
|
44
|
+
def get_logger(self) -> Logger:
|
|
45
|
+
return MaxLogger
|
|
46
|
+
|
|
47
|
+
def get_python_path(self) -> str:
|
|
48
|
+
"""
|
|
49
|
+
返回 3ds Max 的 Python 解释器路径
|
|
50
|
+
|
|
51
|
+
2021 及以上版本: <Max目录>\Python\python.exe
|
|
52
|
+
2020 及以下版本: <Max目录>\3dsmaxpy.exe
|
|
53
|
+
"""
|
|
54
|
+
exe_dir = os.path.dirname(super().get_python_path())
|
|
55
|
+
try:
|
|
56
|
+
import pymxs
|
|
57
|
+
version = pymxs.runtime.maxVersion()
|
|
58
|
+
# 3ds Max 版本号: 2024=26000, 2023=25000, 2020=22000
|
|
59
|
+
if version[0] >= 22000:
|
|
60
|
+
return os.path.join(exe_dir, "Python", "python.exe")
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
# 旧版本使用 3dsmaxpy.exe
|
|
64
|
+
exe_name = "3dsmaxpy.exe" if sys.platform == "win32" else "3dsmaxpy"
|
|
65
|
+
return os.path.join(exe_dir, exe_name)
|
|
66
|
+
|
|
67
|
+
def on_connected(self) -> None:
|
|
68
|
+
try:
|
|
69
|
+
import MaxPlus
|
|
70
|
+
parent_window = MaxPlus.GetQMaxMainWindow()
|
|
71
|
+
except:
|
|
72
|
+
try:
|
|
73
|
+
import qtmax
|
|
74
|
+
parent_window = qtmax.GetQMaxMainWindow()
|
|
75
|
+
except:
|
|
76
|
+
logger = self.get_logger()
|
|
77
|
+
logger.error("无法获取主窗口。MaxPlus 和 qtmax 都不可用。")
|
|
78
|
+
return
|
|
79
|
+
super()._on_connected(parent_window)
|
|
80
|
+
|
|
81
|
+
def add_sys_path(self, path: str) -> None:
|
|
82
|
+
super().add_sys_path(path)
|
|
83
|
+
logger = self.get_logger()
|
|
84
|
+
logger.info(f"Added to {self.name} sys.path: {path}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maya Adapter
|
|
3
|
+
实现 Maya 特定的日志、Python 路径和初始化逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .base import DCCAdapter, Logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MayaLogger(Logger):
|
|
15
|
+
"""Maya 专用日志类"""
|
|
16
|
+
|
|
17
|
+
# channel = "Maya"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def info(cls, message: str):
|
|
21
|
+
super().info(message)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def warn(cls, message: str):
|
|
25
|
+
super().warn(message)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def error(cls, message: str):
|
|
29
|
+
super().error(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MayaAdapter(DCCAdapter):
|
|
33
|
+
"""
|
|
34
|
+
Maya Adapter
|
|
35
|
+
|
|
36
|
+
实现 Maya 特定的逻辑,包括:
|
|
37
|
+
- 日志输出到 Maya 脚本编辑器
|
|
38
|
+
- Python 路径获取(mayapy.exe,而非 maya.exe)
|
|
39
|
+
- 初始化逻辑
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str = "maya"
|
|
43
|
+
|
|
44
|
+
def get_logger(self) -> Logger:
|
|
45
|
+
return MayaLogger
|
|
46
|
+
|
|
47
|
+
def get_python_path(self) -> str:
|
|
48
|
+
"""
|
|
49
|
+
返回 Maya 的 Python 解释器路径(mayapy.exe)
|
|
50
|
+
|
|
51
|
+
Maya 中 sys.executable 是 maya.exe(主程序),
|
|
52
|
+
但 pip 和 debugpy 需要用 mayapy.exe(独立 Python 解释器)。
|
|
53
|
+
mayapy.exe 与 maya.exe 同在 bin 目录下。
|
|
54
|
+
"""
|
|
55
|
+
exe_dir = os.path.dirname(super().get_python_path())
|
|
56
|
+
exe_name = "mayapy.exe" if sys.platform == "win32" else "mayapy"
|
|
57
|
+
return os.path.join(exe_dir, exe_name)
|
|
58
|
+
|
|
59
|
+
def on_connected(self) -> None:
|
|
60
|
+
try:
|
|
61
|
+
# 兼容Pyside2/Pyside6 + 对应shiboken
|
|
62
|
+
try:
|
|
63
|
+
from PySide2.QtWidgets import QWidget
|
|
64
|
+
from shiboken2 import wrapInstance
|
|
65
|
+
except ImportError:
|
|
66
|
+
from PySide6.QtWidgets import QWidget
|
|
67
|
+
from shiboken6 import wrapInstance
|
|
68
|
+
import maya.OpenMayaUI as omui
|
|
69
|
+
parent_window = wrapInstance(int(omui.MQtUtil.mainWindow()), QWidget)
|
|
70
|
+
except:
|
|
71
|
+
logger = self.get_logger()
|
|
72
|
+
logger.error("无法获取主窗口。maya.OpenMayaUI 不可用。")
|
|
73
|
+
return
|
|
74
|
+
super()._on_connected(parent_window)
|
|
75
|
+
|
|
76
|
+
def add_sys_path(self, path: str) -> None:
|
|
77
|
+
super().add_sys_path(path)
|
|
78
|
+
logger = self.get_logger()
|
|
79
|
+
logger.info(f"Added to {self.name} sys.path: {path}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SubstancePainter Adapter
|
|
3
|
+
实现 SubstancePainter 特定的日志、Python 路径和初始化逻辑
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from .base import DCCAdapter, Logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SubstancePainterLogger(Logger):
|
|
15
|
+
"""SubstancePainter 专用日志类"""
|
|
16
|
+
|
|
17
|
+
# channel = "SubstancePainter"
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def info(cls, message: str):
|
|
21
|
+
try:
|
|
22
|
+
import substance_painter as sp
|
|
23
|
+
sp.logging.log(sp.logging.INFO, cls.channel, message)
|
|
24
|
+
except:
|
|
25
|
+
super().info(message)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def warn(cls, message: str):
|
|
29
|
+
try:
|
|
30
|
+
import substance_painter as sp
|
|
31
|
+
sp.logging.log(sp.logging.WARNING, cls.channel, message)
|
|
32
|
+
except:
|
|
33
|
+
super().warn(message)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def error(cls, message: str):
|
|
37
|
+
try:
|
|
38
|
+
import substance_painter as sp
|
|
39
|
+
sp.logging.log(sp.logging.ERROR, cls.channel, message)
|
|
40
|
+
except:
|
|
41
|
+
super().error(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SubstancePainterAdapter(DCCAdapter):
|
|
45
|
+
"""
|
|
46
|
+
SubstancePainter Adapter
|
|
47
|
+
|
|
48
|
+
实现 SubstancePainter 特定的逻辑,包括:
|
|
49
|
+
- 日志输出到 SubstancePainter 脚本编辑器
|
|
50
|
+
- Python 路径获取(python.exe)
|
|
51
|
+
- 初始化逻辑
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str = "substance_painter"
|
|
55
|
+
|
|
56
|
+
def get_logger(self) -> Logger:
|
|
57
|
+
return SubstancePainterLogger
|
|
58
|
+
|
|
59
|
+
def get_python_path(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
返回 SubstancePainter 的 Python 解释器路径(python.exe)
|
|
62
|
+
|
|
63
|
+
所有版本的SP的python解析器路径都位于根目录下的: "./resources/pythonsdk/python.exe"
|
|
64
|
+
"""
|
|
65
|
+
exe_dir = os.path.dirname(super().get_python_path())
|
|
66
|
+
exe_name = "python.exe" if sys.platform == "win32" else "python"
|
|
67
|
+
return os.path.join(exe_dir, "resources", "pythonsdk", exe_name)
|
|
68
|
+
|
|
69
|
+
def on_connected(self) -> None:
|
|
70
|
+
try:
|
|
71
|
+
import substance_painter.ui as sp_ui
|
|
72
|
+
parent_window = sp_ui.get_main_window()
|
|
73
|
+
except:
|
|
74
|
+
logger = self.get_logger()
|
|
75
|
+
logger.error("无法获取主窗口。substance_painter.ui 不可用。")
|
|
76
|
+
return
|
|
77
|
+
super()._on_connected(parent_window)
|
|
78
|
+
|
|
79
|
+
def add_sys_path(self, path: str) -> None:
|
|
80
|
+
super().add_sys_path(path)
|
|
81
|
+
logger = self.get_logger()
|
|
82
|
+
logger.info(f"Added to {self.name} sys.path: {path}")
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dcc cleanup 子命令实现
|
|
3
|
+
|
|
4
|
+
卸载前清理:
|
|
5
|
+
1. 删除 ~/.dcc-bridge 用户数据目录(包含所有发现文件)
|
|
6
|
+
2. 对所有已实现的 DCCSetup 子类执行 unsetup,移除自启动脚本
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
import pkgutil
|
|
13
|
+
import shutil
|
|
14
|
+
from typing import List, Type
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from ..discovery import get_user_data_dir
|
|
19
|
+
from ..setup.base import DCCSetup
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _discover_setup_classes() -> List[Type[DCCSetup]]:
|
|
23
|
+
"""动态发现 dcc_bridge.setup 包下所有非抽象的 DCCSetup 子类"""
|
|
24
|
+
from .. import setup as setup_pkg
|
|
25
|
+
|
|
26
|
+
setup_classes: List[Type[DCCSetup]] = []
|
|
27
|
+
package_name = setup_pkg.__name__
|
|
28
|
+
package_path = setup_pkg.__path__ # type: ignore[attr-defined]
|
|
29
|
+
|
|
30
|
+
# 遍历 setup 包下的所有模块
|
|
31
|
+
for finder, module_name, is_pkg in pkgutil.iter_modules(
|
|
32
|
+
package_path, prefix=f"{package_name}."
|
|
33
|
+
):
|
|
34
|
+
if module_name.endswith(".base") or is_pkg:
|
|
35
|
+
continue
|
|
36
|
+
try:
|
|
37
|
+
module = importlib.import_module(module_name)
|
|
38
|
+
except Exception:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
for attr_name in dir(module):
|
|
42
|
+
obj = getattr(module, attr_name)
|
|
43
|
+
if (
|
|
44
|
+
isinstance(obj, type)
|
|
45
|
+
and issubclass(obj, DCCSetup)
|
|
46
|
+
and obj is not DCCSetup
|
|
47
|
+
and getattr(obj, "dcc_type", "")
|
|
48
|
+
):
|
|
49
|
+
setup_classes.append(obj)
|
|
50
|
+
|
|
51
|
+
return setup_classes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@click.command(name="cleanup")
|
|
55
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.\n跳过确认提示。")
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def cleanup(ctx, yes: bool) -> None:
|
|
58
|
+
"""Clean up dcc-bridge data and auto-startup scripts (use before uninstall).
|
|
59
|
+
|
|
60
|
+
清理 dcc-bridge 数据与自启动脚本(卸载前使用)。
|
|
61
|
+
"""
|
|
62
|
+
user_data_dir = get_user_data_dir()
|
|
63
|
+
|
|
64
|
+
# 先发现所有 setup 子类,用于确认提示
|
|
65
|
+
setup_classes = _discover_setup_classes()
|
|
66
|
+
dcc_names = [cls.dcc_type for cls in setup_classes]
|
|
67
|
+
|
|
68
|
+
if not yes:
|
|
69
|
+
click.echo("即将执行以下清理操作:")
|
|
70
|
+
click.echo(f" 1. 删除用户数据目录: {user_data_dir}")
|
|
71
|
+
if dcc_names:
|
|
72
|
+
click.echo(f" 2. 移除以下 DCC 的自启动脚本: {', '.join(dcc_names)}")
|
|
73
|
+
else:
|
|
74
|
+
click.echo(" 2. 未发现可清理的 DCC 自启动脚本")
|
|
75
|
+
click.echo("")
|
|
76
|
+
if not click.confirm("确认继续?"):
|
|
77
|
+
click.echo("已取消")
|
|
78
|
+
ctx.exit(0)
|
|
79
|
+
|
|
80
|
+
# 1. 删除用户数据目录
|
|
81
|
+
if os.path.exists(user_data_dir):
|
|
82
|
+
try:
|
|
83
|
+
shutil.rmtree(user_data_dir)
|
|
84
|
+
click.echo(f"Removed user data directory: {user_data_dir}")
|
|
85
|
+
except OSError as e:
|
|
86
|
+
click.echo(f"Failed to remove user data directory: {e}", err=True)
|
|
87
|
+
ctx.exit(1)
|
|
88
|
+
else:
|
|
89
|
+
click.echo(f"User data directory not found: {user_data_dir}")
|
|
90
|
+
|
|
91
|
+
# 2. 对所有发现的 DCC 执行 unsetup
|
|
92
|
+
any_failed = False
|
|
93
|
+
for setup_cls in setup_classes:
|
|
94
|
+
dcc_type = setup_cls.dcc_type
|
|
95
|
+
try:
|
|
96
|
+
setup_instance = setup_cls()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
click.echo(f"Failed to instantiate setup for {dcc_type}: {e}", err=True)
|
|
99
|
+
any_failed = True
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
success = setup_instance.unsetup()
|
|
104
|
+
if success:
|
|
105
|
+
click.echo(f"Unsetup succeeded for {dcc_type}")
|
|
106
|
+
else:
|
|
107
|
+
click.echo(f"Unsetup returned failure for {dcc_type} (may not be installed)", err=True)
|
|
108
|
+
any_failed = True
|
|
109
|
+
except Exception as e:
|
|
110
|
+
click.echo(f"Unsetup error for {dcc_type}: {e}", err=True)
|
|
111
|
+
any_failed = True
|
|
112
|
+
|
|
113
|
+
if any_failed:
|
|
114
|
+
ctx.exit(1)
|