@motionharness/bridge-davinci 0.0.0-stage → 0.0.23

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.
@@ -0,0 +1,31 @@
1
+ """File evidence and durable journals shared by the Resolve operations."""
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import uuid
6
+
7
+
8
+ def file_digest(path):
9
+ digest = hashlib.sha256()
10
+ with open(path, "rb") as stream:
11
+ for block in iter(lambda: stream.read(1024 * 1024), b""):
12
+ digest.update(block)
13
+ return "sha256:" + digest.hexdigest()
14
+
15
+
16
+ def atomic_json(path, value, *, allow_nan=True):
17
+ pending = path + "." + str(uuid.uuid4()) + ".pending"
18
+ try:
19
+ with os.fdopen(os.open(pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w", encoding="utf-8") as stream:
20
+ json.dump(value, stream, ensure_ascii=False, allow_nan=allow_nan)
21
+ stream.flush()
22
+ os.fsync(stream.fileno())
23
+ os.replace(pending, path)
24
+ descriptor = os.open(os.path.dirname(path), os.O_RDONLY)
25
+ try:
26
+ os.fsync(descriptor)
27
+ finally:
28
+ os.close(descriptor)
29
+ finally:
30
+ if os.path.exists(pending):
31
+ os.unlink(pending)
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@motionharness/bridge-davinci",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.23",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/VibeMG/vibe-motion-graphics.git",
10
+ "directory": "packages/bridge/davinci"
11
+ },
12
+ "description": "Motion Harness Bridge Component for DaVinci Resolve",
13
+ "exports": {
14
+ ".": "./dist/runner.mjs"
15
+ },
16
+ "files": [
17
+ "dist/runner.mjs",
18
+ "dist/*.py",
19
+ "bridge-manifest.json",
20
+ "resources",
21
+ "README.md",
22
+ "LICENSE",
23
+ "NOTICE",
24
+ "OPENFX_LICENSE",
25
+ "THIRD_PARTY_NOTICES.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.18"
29
+ },
30
+ "os": [
31
+ "darwin"
32
+ ],
33
+ "cpu": [
34
+ "arm64",
35
+ "x64"
36
+ ]
37
+ }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0"><dict>
4
+ <key>CFBundleIdentifier</key><string>com.hotionharness.bridge.davinci</string>
5
+ <key>CFBundleName</key><string>Motion Harness Bridge Component</string>
6
+ <key>CFBundleExecutable</key><string>HotionHarness.ofx</string>
7
+ <key>CFBundlePackageType</key><string>BNDL</string>
8
+ <key>CFBundleVersion</key><string>1</string>
9
+ </dict></plist>
Binary file
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0"><dict>
4
+ <key>CFBundleIdentifier</key><string>com.hotionharness.bridge.status</string>
5
+ <key>CFBundleName</key><string>Motion Harness Bridge Component</string>
6
+ <key>CFBundleDisplayName</key><string>Motion Harness Bridge Component</string>
7
+ <key>CFBundleExecutable</key><string>HotionHarnessStatus</string>
8
+ <key>CFBundlePackageType</key><string>APPL</string>
9
+ <key>CFBundleVersion</key><string>1</string>
10
+ <key>CFBundleShortVersionString</key><string>1.0</string>
11
+ <key>LSUIElement</key><true/>
12
+ </dict></plist>
@@ -0,0 +1,2 @@
1
+ "CFBundleDisplayName" = "Motion Harness Bridge Component";
2
+ "CFBundleName" = "Motion Harness Bridge Component";
@@ -0,0 +1,2 @@
1
+ "CFBundleDisplayName" = "Motion Harness 桥接组件";
2
+ "CFBundleName" = "Motion Harness 桥接组件";
@@ -0,0 +1,55 @@
1
+ -- Motion Harness legacy entry forwarder v1
2
+ -- Explicit button callbacks only; image evaluation never executes this file.
3
+ local helper, config, workspace = @@HELPER@@, @@CONFIG@@, @@WORKSPACE@@
4
+ local function quote(value)
5
+ return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
6
+ end
7
+ local function json_string(value)
8
+ assert(type(value) == 'string', 'Missing legacy entry identity')
9
+ return '"' .. value:gsub('[%z\1-\31\\"]', function(c)
10
+ if c == '\\' then return '\\\\' end
11
+ if c == '"' then return '\\"' end
12
+ return string.format('\\u%04x', string.byte(c))
13
+ end) .. '"'
14
+ end
15
+ local function clicked_target()
16
+ local application = Resolve()
17
+ local manager = application and application:GetProjectManager()
18
+ local project = manager and manager:GetCurrentProject()
19
+ local timeline = project and project:GetCurrentTimeline()
20
+ assert(timeline and not project:IsRenderingInProgress())
21
+ local video = nil
22
+ for _, item in ipairs(timeline:GetSelectedClips() or {}) do
23
+ if string.lower(item:GetType() or '') == 'video' then assert(video == nil); video = item end
24
+ end
25
+ assert(video)
26
+ return { projectId = project:GetUniqueId(), timelineId = timeline:GetUniqueId(), itemId = video:GetUniqueId() }
27
+ end
28
+ return function(request)
29
+ local ok, clicked = pcall(clicked_target)
30
+ local again_ok, again = pcall(clicked_target)
31
+ ok = ok and again_ok and clicked.projectId == again.projectId
32
+ and clicked.timelineId == again.timelineId and clicked.itemId == again.itemId
33
+ and type(request) == 'table' and (request.action == 'edit' or request.action == 'more')
34
+ if ok then
35
+ for _, key in ipairs({'workspaceKey', 'sourceId', 'bindingId'}) do
36
+ if type(request[key]) ~= 'string' or request[key] == '' then ok = false end
37
+ end
38
+ end
39
+ local action, payload = 'legacy', nil
40
+ if ok then
41
+ local fields = {'"workspace":' .. json_string(workspace)}
42
+ for _, key in ipairs({'action', 'workspaceKey', 'sourceId', 'bindingId'}) do
43
+ fields[#fields + 1] = json_string(key) .. ':' .. json_string(request[key])
44
+ end
45
+ fields[#fields + 1] = '"clicked":{"projectId":' .. json_string(clicked.projectId)
46
+ .. ',"timelineId":' .. json_string(clicked.timelineId) .. ',"itemId":' .. json_string(clicked.itemId) .. '}'
47
+ payload = '{' .. table.concat(fields, ',') .. '}'
48
+ else
49
+ action = 'error'
50
+ payload = '{"workspace":' .. json_string(workspace)
51
+ .. ',"code":"SELECTION_UNAVAILABLE","message":"无法确认当前所选片段,请只选择一个 Motion Harness 片段后重试。"}'
52
+ end
53
+ os.execute(quote(helper) .. ' --config ' .. quote(config) .. ' --action ' .. quote(action)
54
+ .. ' --request ' .. quote(payload) .. ' >/dev/null 2>&1 &')
55
+ end
@@ -0,0 +1,31 @@
1
+ -- MH Resolve Bridge inspector v1
2
+ -- Called only by an explicit Inspector button. Nothing executes during rendering.
3
+ local node, cli, workspace = @@NODE@@, @@CLI@@, @@WORKSPACE@@
4
+ local mh, node_dir = @@MH@@, @@NODE_DIR@@
5
+
6
+ local function quote(value)
7
+ return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
8
+ end
9
+
10
+ local function json_string(value)
11
+ assert(type(value) == 'string', 'Hotion Harness Bridge: missing Inspector identity')
12
+ return '"' .. value:gsub('[%z\1-\31\\"]', function(c)
13
+ if c == '\\' then return '\\\\' end
14
+ if c == '"' then return '\\"' end
15
+ return string.format('\\u%04x', string.byte(c))
16
+ end) .. '"'
17
+ end
18
+
19
+ return function(request)
20
+ assert(type(request) == 'table', '请从 Hotion Harness Bridge 检查器按钮打开。')
21
+ assert(request.action == 'edit' or request.action == 'more', 'Hotion Harness Bridge: unknown action')
22
+ local fields = {}
23
+ for _, key in ipairs({'action', 'workspaceKey', 'sourceId', 'bindingId'}) do
24
+ fields[#fields + 1] = json_string(key) .. ':' .. json_string(request[key])
25
+ end
26
+ local payload = '{' .. table.concat(fields, ',') .. '}'
27
+ local command = 'PATH=' .. quote(node_dir) .. ':"$PATH" MH_BRIDGE_MH=' .. quote(mh) .. ' ' .. quote(node) .. ' ' .. quote(cli) .. ' inspector-action --workspace ' .. quote(workspace) .. ' --json ' .. quote(payload)
28
+ -- The controller opens public MH or the Bridge window. This UI callback
29
+ -- never waits on Resolve or MH, and never renders/synchronizes a video.
30
+ os.execute(command .. ' >/dev/null 2>&1 &')
31
+ end
@@ -0,0 +1,164 @@
1
+ """Installed menu: snapshot the selected video, then use the public CLI."""
2
+
3
+ import json
4
+ import hashlib
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+
12
+ _translations = {}
13
+
14
+
15
+ def tr(source):
16
+ return _translations.get(source, source)
17
+
18
+
19
+ class SelectionError(Exception):
20
+ def __init__(self, code, message):
21
+ super().__init__(message)
22
+ self.code = code
23
+
24
+
25
+ def command_environment(config):
26
+ environment = dict(os.environ)
27
+ tools = config.get("tools") if isinstance(config.get("tools"), dict) else {}
28
+ paths = {name: path for name, path in tools.items()
29
+ if name in {"node", "pnpm", "ffmpeg", "ffprobe"}
30
+ and isinstance(path, str) and os.path.isabs(path)}
31
+ allowed = list(dict.fromkeys(os.path.dirname(path) for path in paths.values()))
32
+ preferred = config.get("toolDirectories")
33
+ preferred = preferred if isinstance(preferred, list) else []
34
+ directories = list(dict.fromkeys([value for value in preferred if value in allowed] + allowed))
35
+ if directories:
36
+ environment["PATH"] = os.pathsep.join(directories + [environment.get("PATH", "/usr/bin:/bin")])
37
+ for name in ("ffmpeg", "ffprobe"):
38
+ if name in paths:
39
+ environment["MH_" + name.upper()] = paths[name]
40
+ bridge_directory = config.get("bridgeDirectory")
41
+ if isinstance(bridge_directory, str) and os.path.isabs(bridge_directory):
42
+ environment["MH_BRIDGE_DIR"] = bridge_directory
43
+ environment["MH_UI_LOCALE"] = os.environ.get("MH_UI_LOCALE", config.get("locale", "zh-CN"))
44
+ return environment
45
+
46
+
47
+ def selected_target(application, management=False):
48
+ if not application:
49
+ raise SelectionError("RESOLVE_UNAVAILABLE", tr("达芬奇尚未连接。请打开工程后重试。"))
50
+ manager = application.GetProjectManager()
51
+ project = manager.GetCurrentProject()
52
+ timeline = project.GetCurrentTimeline() if project else None
53
+ if not timeline:
54
+ raise SelectionError("NO_TIMELINE", tr("请打开时间线并选择一个 Motion Harness 片段。"))
55
+ if project.IsRenderingInProgress():
56
+ raise SelectionError("RESOLVE_BUSY", tr("达芬奇正在导出,请完成后重试。"))
57
+ selected = timeline.GetSelectedClips() or []
58
+ identifiers = sorted(item.GetUniqueId() for item in selected)
59
+ videos = [item for item in selected if item.GetType().lower() == "video"]
60
+ if len(videos) != 1:
61
+ raise SelectionError("SELECTION_AMBIGUOUS", tr("请只选择一个 Motion Harness 视频片段。"))
62
+ media = videos[0].GetMediaPoolItem()
63
+ if not media:
64
+ raise SelectionError("NOT_PLAIN_MEDIA", tr("所选片段没有普通视频素材。请先转换旧版生成器。"))
65
+ encoded = (media.GetThirdPartyMetadata() or {}).get("motionharness.bridge") if media else None
66
+ try:
67
+ link = json.loads(encoded) if isinstance(encoded, str) else None
68
+ except (ValueError, TypeError):
69
+ link = None
70
+ linked = isinstance(link, dict) and link.get("schemaVersion") == 1 and all(
71
+ isinstance(link.get(key), str) and link[key] for key in ("bindingId", "sourceId", "artifactId")
72
+ )
73
+ if not linked and not management:
74
+ raise SelectionError("NOT_LINKED", tr("所选视频未关联 Motion Harness 工程。"))
75
+ database = manager.GetCurrentDatabase() if management else None
76
+ if management and (not isinstance(database, dict) or not database):
77
+ raise SelectionError("SCOPE_UNAVAILABLE", tr("无法确认当前工程位置,请检查达芬奇连接。"))
78
+ current = manager.GetCurrentProject()
79
+ active = current.GetCurrentTimeline() if current else None
80
+ if not current or current.GetUniqueId() != project.GetUniqueId() or not active or active.GetUniqueId() != timeline.GetUniqueId() or sorted(
81
+ item.GetUniqueId() for item in (active.GetSelectedClips() or [])
82
+ ) != identifiers:
83
+ raise SelectionError("SELECTION_CHANGED", tr("所选片段已改变,请重新点击。"))
84
+ if management:
85
+ if manager.GetCurrentDatabase() != database:
86
+ raise SelectionError("SELECTION_CHANGED", tr("当前工程位置已改变,请重新点击。"))
87
+ database_id = "sha256:" + hashlib.sha256(json.dumps(database, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest()
88
+ return {"selection": {"scope": {"databaseId": database_id, "projectId": project.GetUniqueId()},
89
+ "timelineId": timeline.GetUniqueId(), "itemId": videos[0].GetUniqueId(), "mediaPoolItemId": media.GetUniqueId(),
90
+ **({key: link[key] for key in ("bindingId", "sourceId", "artifactId")} if linked else {})}}
91
+ return {"bindingId": link["bindingId"], "selection": {
92
+ "projectId": project.GetUniqueId(), "mediaPoolItemId": media.GetUniqueId(),
93
+ "sourceId": link["sourceId"], "artifactId": link["artifactId"]}}
94
+
95
+
96
+ def resolve_application():
97
+ application = globals().get("resolve")
98
+ if application:
99
+ return application
100
+ try:
101
+ import DaVinciResolveScript
102
+ except ImportError:
103
+ # Installed SDK locations, as documented by the Resolve scripting API.
104
+ sdk = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules"
105
+ if sys.platform == "darwin" and sdk not in sys.path:
106
+ sys.path.append(sdk)
107
+ import DaVinciResolveScript
108
+ return DaVinciResolveScript.scriptapp("Resolve")
109
+
110
+
111
+ def main():
112
+ config_path = Path(__HOTION_CONFIG_PATH_JSON__)
113
+ status_path = Path(__HOTION_STATUS_PATH_JSON__)
114
+ action = __HOTION_ACTION_JSON__
115
+ global _translations
116
+ _translations = {}
117
+ repair = None
118
+ try:
119
+ config = json.loads(config_path.read_text(encoding="utf-8"))
120
+ if not isinstance(config, dict):
121
+ raise ValueError("invalid config")
122
+ except (OSError, ValueError, TypeError):
123
+ config = {}
124
+ repair = tr("桥接安装配置缺失或损坏,请运行 mh bridge davinci install 修复组件。")
125
+ if os.environ.get("MH_UI_LOCALE", config.get("locale", "zh-CN")).startswith("en"):
126
+ try:
127
+ translations = json.loads(Path(config["translationsPath"]).read_text(encoding="utf-8"))
128
+ if isinstance(translations, dict):
129
+ _translations = translations
130
+ except (OSError, ValueError, TypeError, KeyError):
131
+ pass
132
+ if not status_path.is_absolute() or not status_path.is_file() or not os.access(status_path, os.X_OK):
133
+ raise RuntimeError(tr("缺少 Motion Harness 状态窗口,请运行 mh bridge davinci install 修复组件。"))
134
+ # The shared helper resolves the installed CLI launcher. Checking the old
135
+ # platform-binary path here would incorrectly reject a completed npm upgrade.
136
+ request = {}
137
+ if isinstance(config.get("workspace"), str):
138
+ request["workspace"] = config["workspace"]
139
+ if repair:
140
+ action = "error"
141
+ request.update(code="INSTALL_MISSING", message=repair)
142
+ else:
143
+ try:
144
+ request.update(selected_target(resolve_application(), management=action == "manage"))
145
+ except SelectionError as error:
146
+ action = "error"
147
+ request.update(code=error.code, message=str(error))
148
+ except (ImportError, AttributeError, RuntimeError):
149
+ action = "error"
150
+ request.update(code="SELECTION_UNAVAILABLE", message=tr("无法读取所选片段,请检查达芬奇脚本连接。"))
151
+ if action == "update":
152
+ request["idempotencyKey"] = str(uuid.uuid4())
153
+ arguments = [str(status_path), "--config", str(config_path), "--action", action,
154
+ "--request", json.dumps(request, ensure_ascii=False)]
155
+ subprocess.Popen(arguments, stdin=subprocess.DEVNULL,
156
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
157
+ env=command_environment(config), close_fds=True, start_new_session=True)
158
+ print(request["message"] if action == "error" else tr("正在打开 Motion Harness 工程…") if action == "edit" else
159
+ tr("正在读取所选 Motion Harness 片段…") if action == "manage" else
160
+ tr("正在提交 Motion Harness 请求,状态窗口将显示进度。"))
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>Motion Harness Bridge Component · Add the sidebar editing entry</title>
5
+ <style>
6
+ :root{color-scheme:dark;font:15px/1.7 system-ui,-apple-system,sans-serif;color:#eee;background:#191a1e}*{box-sizing:border-box}body{max-width:960px;margin:auto;padding:32px 24px}h1{font-size:26px;line-height:1.3;margin:0 0 12px}p{color:#c9cad2}li{margin:14px 0}strong{color:#fff}code,kbd{font:inherit;background:#303139;border-radius:5px;padding:2px 6px}.diagram{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:20px;background:#22242b;border:1px solid #555661;border-radius:10px}.panel{border:1px solid #51545f;border-radius:6px;padding:16px}.panel h2{font-size:15px;margin:0 0 12px}.search,.effect{padding:8px 10px;margin:8px 0;background:#30333e;border-radius:5px}.effect{border-left:3px solid #81aaff}.timeline{grid-column:1/-1;border-top:1px solid #50535f;padding-top:14px}.clip{background:#36496e;border:1px solid #81aaff;border-radius:5px;padding:12px}.caption{font-size:13px}aside{border-top:1px solid #40414b;margin-top:24px;padding-top:12px}@media(max-width:600px){body{padding:20px 16px}.diagram{grid-template-columns:1fr}.timeline{grid-column:auto}}
7
+ </style>
8
+ <h1>Add the sidebar editing entry</h1><p>The media sent through the bridge is already linked to its source project. Adding this optional effect does not render the video again.</p>
9
+ <ol><li>On the <strong>Edit</strong> page in DaVinci Resolve, open the <strong>Effects Library</strong> (magic wand icon).</li><li>Search for <strong>Motion Harness Bridge Component</strong> and drag the Open FX effect onto the target animation clip.</li><li>Select the clip and click <strong>Edit Motion Harness project</strong> under <strong>Inspector → Effects → Open FX</strong>.</li></ol>
10
+ <div class="diagram" role="img" aria-label="Illustration: Effects Library, inspector and target clip">
11
+ <section class="panel"><h2>① Effects Library</h2><div class="search">⌕ Motion Harness</div><div class="effect">② Motion Harness Bridge Component</div></section>
12
+ <section class="panel"><h2>Inspector → Effects → Open FX</h2><div class="effect">Edit Motion Harness project</div><p>Update this media · Check status · More…</p></section>
13
+ <div class="timeline"><div class="clip">③ Target animation clip · Add the effect here</div></div></div>
14
+ <p class="caption">Layout illustration. Labels follow the language selected when installing the component. Search for “Motion Harness” in either language.</p><aside><p><strong>Shortcut:</strong> Select the clip, press <kbd>Shift</kbd> + <kbd>Space</kbd>, and search for Motion Harness. If your shortcuts are customized, use the Effects Library.</p><p><strong>Cannot find the effect?</strong> Install the Motion Harness Bridge Component with <code>mh bridge davinci install</code>. Save your project and restart DaVinci Resolve after installation.</p><p><strong>Menu alternative:</strong> Workspace → Scripts → Utility → Motion Harness Bridge Component → Edit project. Clips with the effect already attached do not need it added again.</p></aside>
15
+ </html>
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>Motion Harness 桥接组件 · 添加侧栏编辑入口</title>
5
+ <style>
6
+ :root{color-scheme:dark;font:15px/1.7 system-ui,-apple-system,sans-serif;color:#eee;background:#191a1e}*{box-sizing:border-box}body{max-width:960px;margin:auto;padding:32px 24px}h1{font-size:26px;line-height:1.3;margin:0 0 12px}p{color:#c9cad2}li{margin:14px 0}strong{color:#fff}code,kbd{font:inherit;background:#303139;border-radius:5px;padding:2px 6px}.diagram{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:20px;background:#22242b;border:1px solid #555661;border-radius:10px}.panel{border:1px solid #51545f;border-radius:6px;padding:16px}.panel h2{font-size:15px;margin:0 0 12px}.search,.effect{padding:8px 10px;margin:8px 0;background:#30333e;border-radius:5px}.effect{border-left:3px solid #81aaff}.timeline{grid-column:1/-1;border-top:1px solid #50535f;padding-top:14px}.clip{background:#36496e;border:1px solid #81aaff;border-radius:5px;padding:12px}.caption{font-size:13px}aside{border-top:1px solid #40414b;margin-top:24px;padding-top:12px}@media(max-width:600px){body{padding:20px 16px}.diagram{grid-template-columns:1fr}.timeline{grid-column:auto}}
7
+ </style>
8
+ <h1>添加侧栏编辑入口</h1><p>通过桥接发送的素材已关联源工程。添加这个可选效果不会重新导出视频。</p>
9
+ <ol><li>进入达芬奇的<strong>「剪辑」页</strong>,打开左上角的<strong>「特效库」</strong>(魔棒图标)。</li><li>搜索 <strong>Motion Harness 桥接组件</strong>,将 Open FX 中的同名效果拖到时间线上的目标动效片段。</li><li>选中片段,在<strong>「检查器 → 特效 → Open FX」</strong>中点击<strong>「编辑 Motion Harness 工程」</strong>。</li></ol>
10
+ <div class="diagram" role="img" aria-label="特效库、检查器和目标片段的布局示意">
11
+ <section class="panel"><h2>① 特效库</h2><div class="search">⌕ Motion Harness</div><div class="effect">② Motion Harness 桥接组件</div></section>
12
+ <section class="panel"><h2>检查器 → 特效 → Open FX</h2><div class="effect">编辑 Motion Harness 工程</div><p>更新此素材 · 查看状态 · 更多…</p></section>
13
+ <div class="timeline"><div class="clip">③ 目标动效片段 · 将效果拖到这里</div></div></div>
14
+ <p class="caption">布局示意图。名称按组件安装时的语言显示;中英文均可搜索“Motion Harness”。</p><aside><p><strong>快捷方式:</strong>选中片段,按 <kbd>Shift</kbd> + <kbd>Space</kbd>,搜索 Motion Harness 并添加。快捷键被自定义时,请使用特效库。</p><p><strong>找不到效果?</strong>先安装 Motion Harness 桥接组件:<code>mh bridge davinci install</code>。安装完成后保存工程并重启达芬奇。</p><p><strong>也可以使用菜单:</strong>工作区 → 脚本 → Utility → Motion Harness 桥接组件 → 编辑工程。已经添加过效果的片段无需重复添加。</p></aside>
15
+ </html>