cozyclay 1.2.0 → 1.3.0
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.
- package/CHANGELOG.md +85 -0
- package/README.md +31 -0
- package/THIRD_PARTY_NOTICES.md +37 -1
- package/bin/cozyclay.mjs +51 -2
- package/dist/ai-camera-control/index.html +406 -0
- package/dist/app/index.html +5 -5
- package/dist/assets/app-B3U5aut1.js +4811 -0
- package/dist/assets/app-BrRF0wso.css +1 -0
- package/dist/assets/vision_bundle-jFkh-fIS.js +41 -0
- package/dist/fonts/InstrumentSerif-OFL.txt +93 -0
- package/dist/fonts/Inter-OFL.txt +92 -0
- package/dist/fonts/README.md +15 -0
- package/dist/index.html +60 -16
- package/dist/sitemap.xml +7 -1
- package/mcp/LIVE-PROTOCOL.md +63 -0
- package/mcp/README.md +142 -0
- package/mcp/ardy-prompts.mjs +170 -0
- package/mcp/live-hub.mjs +105 -0
- package/mcp/package.json +24 -0
- package/mcp/server.mjs +1394 -0
- package/package.json +122 -90
- package/src/App.jsx +2957 -514
- package/src/ardy/cskel27.js +7 -2
- package/src/ardy/ik.js +25 -15
- package/src/ardy/npz.js +64 -3
- package/src/ardy/playback.js +31 -1
- package/src/ardy/prompt-clips.js +7 -2
- package/src/ardy/retime.js +211 -0
- package/src/ardy/timeline-coordinates.js +13 -0
- package/src/ardy/timeline.jsx +124 -5
- package/src/ardy/to-cskel27.js +34 -12
- package/src/ardy/trim.js +33 -0
- package/src/asset-pane.jsx +36 -0
- package/src/dualview.jsx +14 -8
- package/src/hierarchy-model.js +95 -13
- package/src/hierarchy-panel.jsx +139 -6
- package/src/live-control.js +122 -0
- package/src/matte-editor.js +543 -0
- package/src/matte.js +503 -0
- package/src/multimodel-ingest.js +344 -0
- package/src/object-gizmo.jsx +43 -15
- package/src/planview.jsx +43 -32
- package/src/pose-extract/detector.js +75 -0
- package/src/pose-extract/index.js +3 -0
- package/src/pose-extract/take.js +87 -0
- package/src/pose-extract/video-frames.js +91 -0
- package/src/pose-thumbs.js +152 -0
- package/src/posestudio.jsx +361 -11
- package/src/project-browser.jsx +135 -0
- package/src/project.js +289 -0
- package/src/props.jsx +69 -3
- package/src/room.jsx +14 -35
- package/src/scene-asset-cache.js +125 -0
- package/src/scene-assets.js +288 -0
- package/src/scene-objects.js +245 -7
- package/src/scenes.js +207 -26
- package/src/shot-authoring.js +55 -13
- package/src/styles.css +1296 -129
- package/tools/ardy/BRIDGE.md +3 -2
- package/tools/ardy/README.md +9 -5
- package/tools/ardy/bridge.mjs +57 -1
- package/tools/ardy/bvh-cskel27.mjs +1209 -0
- package/tools/ardy/cclay_constrained_generate.py +123 -11
- package/tools/ardy/cclay_sequence_generate.py +49 -0
- package/tools/ardy/extract.mjs +367 -0
- package/tools/ardy/footage.mjs +462 -0
- package/tools/ardy/npz.mjs +74 -9
- package/tools/ardy/run-on-box.sh +25 -0
- package/tools/ardy/run-sequence-on-box.sh +15 -0
- package/tools/ardy/runners/remote.mjs +8 -2
- package/dist/assets/app-Cgpk2hwX.js +0 -4803
- package/dist/assets/app-DgZvaAE1.css +0 -1
package/src/hierarchy-panel.jsx
CHANGED
|
@@ -11,18 +11,115 @@ const HIERARCHY_LABELS_KO = {
|
|
|
11
11
|
"Character 1": "캐릭터 1",
|
|
12
12
|
"Character 2": "캐릭터 2",
|
|
13
13
|
Rig: "리그",
|
|
14
|
+
Torso: "몸통",
|
|
14
15
|
"Root / Hips": "루트 / 골반",
|
|
15
16
|
Spine: "척추",
|
|
17
|
+
Chest: "가슴",
|
|
18
|
+
Neck: "목",
|
|
19
|
+
Head: "머리",
|
|
16
20
|
"Left Arm": "왼팔",
|
|
21
|
+
"Left Shoulder": "왼쪽 어깨",
|
|
22
|
+
"Left Elbow": "왼쪽 팔꿈치",
|
|
23
|
+
"Left Hand": "왼손",
|
|
17
24
|
"Right Arm": "오른팔",
|
|
25
|
+
"Right Shoulder": "오른쪽 어깨",
|
|
26
|
+
"Right Elbow": "오른쪽 팔꿈치",
|
|
27
|
+
"Right Hand": "오른손",
|
|
18
28
|
"Left Leg": "왼다리",
|
|
29
|
+
"Left Knee": "왼쪽 무릎",
|
|
30
|
+
"Left Foot": "왼발",
|
|
19
31
|
"Right Leg": "오른다리",
|
|
32
|
+
"Right Knee": "오른쪽 무릎",
|
|
33
|
+
"Right Foot": "오른발",
|
|
20
34
|
Environment: "환경",
|
|
21
35
|
Props: "소품",
|
|
22
36
|
};
|
|
23
37
|
|
|
24
38
|
const FALLBACK_SCENES = [{ id: "current-scene", name: "SCENE 01" }];
|
|
25
39
|
|
|
40
|
+
function HierarchyIcon({ kind, className = "" }) {
|
|
41
|
+
const common = {
|
|
42
|
+
className: `hierarchy-icon ${kind}${className ? ` ${className}` : ""}`,
|
|
43
|
+
viewBox: "0 0 24 24",
|
|
44
|
+
fill: "none",
|
|
45
|
+
stroke: "currentColor",
|
|
46
|
+
strokeWidth: 1.7,
|
|
47
|
+
strokeLinecap: "round",
|
|
48
|
+
strokeLinejoin: "round",
|
|
49
|
+
"aria-hidden": true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
switch (kind) {
|
|
53
|
+
case "scene":
|
|
54
|
+
return (
|
|
55
|
+
<svg {...common}>
|
|
56
|
+
<path d="M12 3.25 21 8l-9 4.75L3 8l9-4.75Z" fill="currentColor" fillOpacity=".16" />
|
|
57
|
+
<path d="m4.25 12 7.75 4.1 7.75-4.1M4.25 16 12 20.1l7.75-4.1" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
case "camera":
|
|
61
|
+
return (
|
|
62
|
+
<svg {...common}>
|
|
63
|
+
<rect x="3" y="6.5" width="13.5" height="11" rx="2" />
|
|
64
|
+
<path d="m16.5 10 4.5-2.25v8.5L16.5 14" />
|
|
65
|
+
<circle cx="9.5" cy="12" r="2.4" />
|
|
66
|
+
</svg>
|
|
67
|
+
);
|
|
68
|
+
case "group":
|
|
69
|
+
return (
|
|
70
|
+
<svg {...common}>
|
|
71
|
+
<circle cx="9" cy="8.25" r="2.75" />
|
|
72
|
+
<circle cx="16.5" cy="9.25" r="2.15" />
|
|
73
|
+
<path d="M3.75 18c.45-3.25 2.15-5 5.25-5s4.8 1.75 5.25 5M14 14.25c3.55-.55 5.65.75 6.25 3.75" />
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
case "character":
|
|
77
|
+
return (
|
|
78
|
+
<svg {...common}>
|
|
79
|
+
<circle cx="12" cy="7.25" r="3" fill="currentColor" fillOpacity=".12" />
|
|
80
|
+
<path d="M5.25 20c.45-5 2.7-7.5 6.75-7.5S18.3 15 18.75 20M9.25 13.25 12 16l2.75-2.75" />
|
|
81
|
+
</svg>
|
|
82
|
+
);
|
|
83
|
+
case "rig":
|
|
84
|
+
return (
|
|
85
|
+
<svg {...common}>
|
|
86
|
+
<circle cx="12" cy="4.5" r="1.75" fill="currentColor" fillOpacity=".18" />
|
|
87
|
+
<circle cx="6" cy="11.5" r="1.75" />
|
|
88
|
+
<circle cx="18" cy="11.5" r="1.75" />
|
|
89
|
+
<circle cx="12" cy="19.5" r="1.75" />
|
|
90
|
+
<path d="m10.8 5.8-3.6 4.4m6-4.4 3.6 4.4M7.75 12.65l3 5.4m5.5-5.4-3 5.4" />
|
|
91
|
+
</svg>
|
|
92
|
+
);
|
|
93
|
+
case "bone":
|
|
94
|
+
return (
|
|
95
|
+
<svg {...common}>
|
|
96
|
+
<path d="M7.4 8.25a2.5 2.5 0 1 1-3.65-3.4A2.5 2.5 0 1 1 7.2 8.5l8.3 8.3a2.5 2.5 0 1 1 3.65 3.4 2.5 2.5 0 1 1-3.4-3.65L7.4 8.25Z" />
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
case "environment":
|
|
100
|
+
return (
|
|
101
|
+
<svg {...common}>
|
|
102
|
+
<circle cx="12" cy="12" r="8.5" fill="currentColor" fillOpacity=".08" />
|
|
103
|
+
<path d="M3.8 13h16.4M12 3.5c2.25 2.35 3.4 5.2 3.4 8.5S14.25 18.15 12 20.5C9.75 18.15 8.6 15.3 8.6 12S9.75 5.85 12 3.5Z" />
|
|
104
|
+
</svg>
|
|
105
|
+
);
|
|
106
|
+
case "props":
|
|
107
|
+
case "object":
|
|
108
|
+
return (
|
|
109
|
+
<svg {...common}>
|
|
110
|
+
<path d="m12 3.5 8 4.25v8.5l-8 4.25-8-4.25v-8.5L12 3.5Z" fill="currentColor" fillOpacity=".1" />
|
|
111
|
+
<path d="m4.25 7.9 7.75 4.2 7.75-4.2M12 12.1v8.15" />
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
default:
|
|
115
|
+
return (
|
|
116
|
+
<svg {...common}>
|
|
117
|
+
<circle cx="12" cy="12" r="7.5" />
|
|
118
|
+
</svg>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
26
123
|
function displaySceneName(name) {
|
|
27
124
|
if (!isKo) return name;
|
|
28
125
|
const generatedName = /^SCENE\s+(\d+)$/i.exec(name);
|
|
@@ -108,7 +205,7 @@ function SceneSwitcher({
|
|
|
108
205
|
}}
|
|
109
206
|
onDoubleClick={() => setEditingId(scene.id)}
|
|
110
207
|
>
|
|
111
|
-
<
|
|
208
|
+
<HierarchyIcon kind="scene" className="scene-document-icon" />
|
|
112
209
|
<span>{displaySceneName(scene.name)}</span>
|
|
113
210
|
</button>
|
|
114
211
|
)}
|
|
@@ -212,8 +309,33 @@ function TreeRow({
|
|
|
212
309
|
onRenameCommit,
|
|
213
310
|
onRenameCancel,
|
|
214
311
|
onRowContextMenu,
|
|
312
|
+
dropHandlers,
|
|
215
313
|
}) {
|
|
216
314
|
const branch = !!node.children?.length;
|
|
315
|
+
// The drop BEHAVIOUR is shared by every row that accepts a picture; the
|
|
316
|
+
// highlight is not. One row lit under the cursor says where the file will
|
|
317
|
+
// land; a whole branch lighting up says nothing.
|
|
318
|
+
const [dropOver, setDropOver] = useState(false);
|
|
319
|
+
const dropDepth = useRef(0);
|
|
320
|
+
const drop = dropHandlers && {
|
|
321
|
+
onDragEnter: (event) => {
|
|
322
|
+
dropHandlers.onDragEnter(event);
|
|
323
|
+
if (!event.dataTransfer?.types?.includes?.("Files")) return;
|
|
324
|
+
dropDepth.current += 1;
|
|
325
|
+
setDropOver(true);
|
|
326
|
+
},
|
|
327
|
+
onDragOver: dropHandlers.onDragOver,
|
|
328
|
+
onDragLeave: (event) => {
|
|
329
|
+
dropHandlers.onDragLeave(event);
|
|
330
|
+
dropDepth.current = Math.max(0, dropDepth.current - 1);
|
|
331
|
+
if (!dropDepth.current) setDropOver(false);
|
|
332
|
+
},
|
|
333
|
+
onDrop: (event) => {
|
|
334
|
+
dropDepth.current = 0;
|
|
335
|
+
setDropOver(false);
|
|
336
|
+
dropHandlers.onDrop(event);
|
|
337
|
+
},
|
|
338
|
+
};
|
|
217
339
|
const label = displayHierarchyLabel(node);
|
|
218
340
|
const rowWrapRef = useRef(null);
|
|
219
341
|
const inputRef = useRef(null);
|
|
@@ -241,6 +363,8 @@ function TreeRow({
|
|
|
241
363
|
className={"hierarchy-row-wrap" + (selectedId === node.id ? " selected" : "")}
|
|
242
364
|
style={{ "--hierarchy-depth": depth }}
|
|
243
365
|
data-node-id={node.id}
|
|
366
|
+
data-drop={drop ? (dropOver ? "over" : "target") : undefined}
|
|
367
|
+
{...(drop ?? {})}
|
|
244
368
|
role="treeitem"
|
|
245
369
|
tabIndex={-1}
|
|
246
370
|
aria-selected={selectedId === node.id}
|
|
@@ -264,7 +388,7 @@ function TreeRow({
|
|
|
264
388
|
// blur commits (docs/unity-reference.md §9.7). A div, not the
|
|
265
389
|
// row button — an input inside a button is invalid HTML.
|
|
266
390
|
<div className="hierarchy-row">
|
|
267
|
-
<
|
|
391
|
+
<HierarchyIcon kind={node.kind} />
|
|
268
392
|
<input
|
|
269
393
|
ref={inputRef}
|
|
270
394
|
className="hierarchy-rename-input"
|
|
@@ -287,7 +411,7 @@ function TreeRow({
|
|
|
287
411
|
</div>
|
|
288
412
|
) : (
|
|
289
413
|
<button type="button" className="hierarchy-row" onClick={() => onSelect(node.id)}>
|
|
290
|
-
<
|
|
414
|
+
<HierarchyIcon kind={node.kind} />
|
|
291
415
|
<span className="hierarchy-label">{label}</span>
|
|
292
416
|
{status && <span className="hierarchy-status">{status}</span>}
|
|
293
417
|
{badge !== null && badge !== undefined && badge !== 0 && <span className="hierarchy-badge">{badge}</span>}
|
|
@@ -301,6 +425,7 @@ export default function HierarchyPanel({
|
|
|
301
425
|
selectedId,
|
|
302
426
|
onSelect,
|
|
303
427
|
showB,
|
|
428
|
+
characters = null,
|
|
304
429
|
motionFrames,
|
|
305
430
|
ikMode,
|
|
306
431
|
sceneObjects = [],
|
|
@@ -309,6 +434,7 @@ export default function HierarchyPanel({
|
|
|
309
434
|
onDuplicateObject,
|
|
310
435
|
onDeleteObject,
|
|
311
436
|
onFrameObject,
|
|
437
|
+
propsDrop = null,
|
|
312
438
|
scenes,
|
|
313
439
|
activeSceneId,
|
|
314
440
|
onSceneSelect,
|
|
@@ -331,9 +457,9 @@ export default function HierarchyPanel({
|
|
|
331
457
|
return (availableScenes.find((scene) => scene.id === activeSceneId) ?? availableScenes[0]).name;
|
|
332
458
|
}, [activeSceneId, scenes]);
|
|
333
459
|
const hierarchyNodes = useMemo(() => {
|
|
334
|
-
const nodes = buildHierarchyNodes(sceneObjects);
|
|
460
|
+
const nodes = buildHierarchyNodes(sceneObjects, characters);
|
|
335
461
|
return nodes.map((node) => node.kind === "scene" ? { ...node, label: activeSceneName } : node);
|
|
336
|
-
}, [activeSceneName, sceneObjects]);
|
|
462
|
+
}, [activeSceneName, sceneObjects, characters]);
|
|
337
463
|
const parents = useMemo(() => indexParents(hierarchyNodes), [hierarchyNodes]);
|
|
338
464
|
|
|
339
465
|
useEffect(() => {
|
|
@@ -384,7 +510,9 @@ export default function HierarchyPanel({
|
|
|
384
510
|
});
|
|
385
511
|
};
|
|
386
512
|
const badgeFor = (id) => {
|
|
387
|
-
if (id === "characters")
|
|
513
|
+
if (id === "characters") {
|
|
514
|
+
return Array.isArray(characters) ? characters.filter((entry) => !entry.hidden).length : (showB ? 2 : 1);
|
|
515
|
+
}
|
|
388
516
|
if (id === "props") return sceneObjects.length;
|
|
389
517
|
return null;
|
|
390
518
|
};
|
|
@@ -466,6 +594,11 @@ export default function HierarchyPanel({
|
|
|
466
594
|
onRenameCommit={(name) => commitRename(node.id, name)}
|
|
467
595
|
onRenameCancel={cancelRename}
|
|
468
596
|
onRowContextMenu={openRowMenu}
|
|
597
|
+
// A picture dropped on Props — or on any prop already in it —
|
|
598
|
+
// becomes a cutout in the set. Dropping ON an object is the
|
|
599
|
+
// same gesture as dropping on the group it lives in: people
|
|
600
|
+
// aim at the list, not at the heading.
|
|
601
|
+
dropHandlers={propsDrop && (node.kind === "props" || node.kind === "object") ? propsDrop.handlers : null}
|
|
469
602
|
/>,
|
|
470
603
|
...(node.children && open ? renderNodes(node.children, depth + 1) : []),
|
|
471
604
|
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Browser-side client for the editor half of mcp/LIVE-PROTOCOL.md. This
|
|
2
|
+
// module deliberately has no browser imports so its frame dispatcher is
|
|
3
|
+
// directly testable in Node with a fake WebSocket.
|
|
4
|
+
|
|
5
|
+
export const LIVE_CONTROL_URL = "ws://127.0.0.1:5184/live";
|
|
6
|
+
export const LIVE_CONTROL_RECONNECT_MS = 3000;
|
|
7
|
+
|
|
8
|
+
function errorMessage(error) {
|
|
9
|
+
if (error instanceof Error && error.message) return error.message;
|
|
10
|
+
return typeof error === "string" && error ? error : "Command failed";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function result(id, ok, body) {
|
|
14
|
+
return ok
|
|
15
|
+
? { type: "result", id, ok: true, value: body ?? {} }
|
|
16
|
+
: { type: "result", id, ok: false, error: body };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse and dispatch one incoming text frame. Non-command frames are ignored;
|
|
21
|
+
* a command with a valid id always receives a protocol result, including for
|
|
22
|
+
* malformed arguments and unknown command names.
|
|
23
|
+
*/
|
|
24
|
+
export async function dispatchLiveFrame(data, handlers = {}) {
|
|
25
|
+
if (typeof data !== "string") return null;
|
|
26
|
+
let frame;
|
|
27
|
+
try {
|
|
28
|
+
frame = JSON.parse(data);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (!frame || typeof frame !== "object" || Array.isArray(frame) || frame.type !== "cmd") return null;
|
|
33
|
+
if (typeof frame.id !== "string") return null;
|
|
34
|
+
if (typeof frame.name !== "string" || !frame.name) return result(frame.id, false, "Invalid command name");
|
|
35
|
+
if (!frame.args || typeof frame.args !== "object" || Array.isArray(frame.args)) return result(frame.id, false, "Invalid command arguments");
|
|
36
|
+
const handler = handlers[frame.name];
|
|
37
|
+
if (typeof handler !== "function") return result(frame.id, false, `Unknown command: ${frame.name}`);
|
|
38
|
+
try {
|
|
39
|
+
return result(frame.id, true, await handler(frame.args));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return result(frame.id, false, errorMessage(error));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Open the editor's one-way client connection. Failures are intentionally
|
|
47
|
+
* silent: a studio remains a fully local editor when the MCP server is absent.
|
|
48
|
+
*/
|
|
49
|
+
export function createLiveControl({
|
|
50
|
+
handlers = {},
|
|
51
|
+
WebSocketImpl = globalThis.WebSocket,
|
|
52
|
+
url = LIVE_CONTROL_URL,
|
|
53
|
+
reconnectMs = LIVE_CONTROL_RECONNECT_MS,
|
|
54
|
+
} = {}) {
|
|
55
|
+
let currentHandlers = handlers;
|
|
56
|
+
let socket = null;
|
|
57
|
+
let retry = null;
|
|
58
|
+
let stopped = false;
|
|
59
|
+
|
|
60
|
+
const clearRetry = () => {
|
|
61
|
+
if (retry !== null) clearTimeout(retry);
|
|
62
|
+
retry = null;
|
|
63
|
+
};
|
|
64
|
+
const scheduleReconnect = () => {
|
|
65
|
+
if (stopped || retry !== null) return;
|
|
66
|
+
retry = setTimeout(() => {
|
|
67
|
+
retry = null;
|
|
68
|
+
connect();
|
|
69
|
+
}, reconnectMs);
|
|
70
|
+
};
|
|
71
|
+
const send = (frame) => {
|
|
72
|
+
if (!socket || socket.readyState !== (socket.OPEN ?? 1)) return;
|
|
73
|
+
try {
|
|
74
|
+
socket.send(JSON.stringify(frame));
|
|
75
|
+
} catch {
|
|
76
|
+
// A close between readyState and send is indistinguishable from an
|
|
77
|
+
// absent server to the editor, so leave it silent and retry on close.
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const connect = () => {
|
|
81
|
+
if (stopped || !WebSocketImpl) return;
|
|
82
|
+
try {
|
|
83
|
+
socket = new WebSocketImpl(url);
|
|
84
|
+
} catch {
|
|
85
|
+
scheduleReconnect();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const connected = socket;
|
|
89
|
+
connected.onopen = () => {
|
|
90
|
+
if (socket !== connected || stopped) return;
|
|
91
|
+
send({ type: "hello", role: "editor", version: 1 });
|
|
92
|
+
};
|
|
93
|
+
connected.onmessage = async (event) => {
|
|
94
|
+
const response = await dispatchLiveFrame(event?.data, currentHandlers);
|
|
95
|
+
if (response) send(response);
|
|
96
|
+
};
|
|
97
|
+
// Suppress browser error reporting for an optional local endpoint.
|
|
98
|
+
connected.onerror = () => {};
|
|
99
|
+
connected.onclose = () => {
|
|
100
|
+
if (socket === connected) socket = null;
|
|
101
|
+
scheduleReconnect();
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
connect();
|
|
106
|
+
return {
|
|
107
|
+
setHandlers(nextHandlers) {
|
|
108
|
+
currentHandlers = nextHandlers && typeof nextHandlers === "object" ? nextHandlers : {};
|
|
109
|
+
},
|
|
110
|
+
close() {
|
|
111
|
+
stopped = true;
|
|
112
|
+
clearRetry();
|
|
113
|
+
const current = socket;
|
|
114
|
+
socket = null;
|
|
115
|
+
try {
|
|
116
|
+
current?.close();
|
|
117
|
+
} catch {
|
|
118
|
+
// Optional transport cleanup must not affect the editor.
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|