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 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,10 @@
1
+ """
2
+ 支持 `python -m dcc_bridge` 调用,转发到 CLI 主入口。
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dcc_bridge.cli.main import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -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,10 @@
1
+ """
2
+ dcc-bridge CLI 子包
3
+
4
+ 提供 `dcc` 命令行工具,用于:
5
+ - 在 DCC 中执行代码/文件(dcc run)
6
+ - 注入/移除 DCC 自启动脚本(dcc setup / unsetup)
7
+ - 列出运行中的 DCC 实例(dcc list / status / ping)
8
+ """
9
+
10
+ from __future__ import annotations
@@ -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)