245 lines
7.0 KiB
JavaScript
245 lines
7.0 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
// AE Timeline dialog singleton
|
|
let aeTimelineDialog = null;
|
|
|
|
// 静态目录 WEB_DIRECTORY = "./js",对外路径无需再带 /js 前缀
|
|
const ASSET_BASE = "/extensions/ComfyUI-AE-Animation/vue-dist";
|
|
|
|
async function ensureVueTimelineLoaded() {
|
|
// Always ensure CSS is present (even if createTimelineApp already exists)
|
|
if (!document.querySelector('link[data-ae-timeline-css="1"]')) {
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = `${ASSET_BASE}/assets/timeline.css`;
|
|
link.dataset.aeTimelineCss = "1";
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
if (!document.querySelector('link[data-ae-mask-css="1"]')) {
|
|
const maskLink = document.createElement("link");
|
|
maskLink.rel = "stylesheet";
|
|
maskLink.href = `${ASSET_BASE}/assets/mask-editor.css`;
|
|
maskLink.dataset.aeMaskCss = "1";
|
|
document.head.appendChild(maskLink);
|
|
}
|
|
|
|
// Load entry module only if not already present
|
|
if (!window.createTimelineApp) {
|
|
const script = document.createElement("script");
|
|
script.type = "module";
|
|
script.src = `${ASSET_BASE}/timeline.js`;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
|
|
// Give the module a brief moment to initialize
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
}
|
|
|
|
async function openAETimelineForNode(node) {
|
|
await ensureVueTimelineLoaded();
|
|
|
|
// Close existing dialog if present
|
|
if (aeTimelineDialog) {
|
|
try {
|
|
aeTimelineDialog.close();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
aeTimelineDialog.remove();
|
|
aeTimelineDialog = null;
|
|
}
|
|
|
|
const dialog = document.createElement("dialog");
|
|
dialog.className = "ae-timeline-dialog";
|
|
Object.assign(dialog.style, {
|
|
position: "fixed",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
width: "min(98vw, 2400px)",
|
|
height: "min(96vh, 1400px)",
|
|
maxWidth: "98vw",
|
|
maxHeight: "96vh",
|
|
padding: "0",
|
|
margin: "0",
|
|
border: "1px solid #444",
|
|
borderRadius: "8px",
|
|
background: "#1a1a1a",
|
|
zIndex: "10000",
|
|
boxSizing: "border-box",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
overflow: "hidden",
|
|
});
|
|
|
|
const container = document.createElement("div");
|
|
// Vue 组件会创建 .ae-timeline-root 作为根元素,这里不需要额外样式
|
|
container.style.cssText = "width: 100%; height: 100%;";
|
|
|
|
dialog.appendChild(container);
|
|
document.body.appendChild(dialog);
|
|
|
|
if (!window.createTimelineApp) {
|
|
container.innerHTML = '<div style="padding:20px;color:#f44">Failed to load AE Timeline Vue app. Please check console.</div>';
|
|
dialog.showModal();
|
|
return;
|
|
}
|
|
|
|
const vueApp = window.createTimelineApp(container, { node });
|
|
|
|
dialog.addEventListener("close", () => {
|
|
try {
|
|
vueApp.unmount && vueApp.unmount();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
// 自动保存一次节点,避免丢失
|
|
if (node && typeof node.save === "function") node.save();
|
|
const graph = getGraph();
|
|
if (graph?.setDirtyCanvas) graph.setDirtyCanvas(true, true);
|
|
} catch (e) {
|
|
console.warn("[AE Timeline] autosave on close failed", e);
|
|
}
|
|
dialog.remove();
|
|
if (aeTimelineDialog === dialog) {
|
|
aeTimelineDialog = null;
|
|
}
|
|
});
|
|
|
|
dialog.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") {
|
|
dialog.close();
|
|
}
|
|
});
|
|
|
|
aeTimelineDialog = dialog;
|
|
dialog.showModal();
|
|
}
|
|
|
|
function getGraph() {
|
|
return (
|
|
app?.graph ||
|
|
app?.canvas?.graph ||
|
|
(window.LGraphCanvas && window.LGraphCanvas.active_canvas && window.LGraphCanvas.active_canvas.graph) ||
|
|
null
|
|
);
|
|
}
|
|
|
|
function getSelectedAEAnimationNode() {
|
|
const canvas = window.LGraphCanvas && window.LGraphCanvas.active_canvas;
|
|
if (!canvas) return null;
|
|
const selected = canvas.selected_nodes || {};
|
|
const nodes = Object.values(selected);
|
|
return nodes.find((n) => n && n.constructor && n.constructor.comfyClass === "AEAnimation") || null;
|
|
}
|
|
|
|
function findExistingAEAnimation(graph) {
|
|
if (!graph) return null;
|
|
if (typeof graph.findNodesByType === "function") {
|
|
const list = graph.findNodesByType("AEAnimation");
|
|
if (list && list.length) return list[0];
|
|
}
|
|
if (Array.isArray(graph._nodes)) {
|
|
const found = graph._nodes.find((n) => n?.constructor?.comfyClass === "AEAnimation");
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function createAEAnimationNode() {
|
|
try {
|
|
const LiteGraph = window.LiteGraph;
|
|
const graph = getGraph();
|
|
if (!LiteGraph || !graph) return null;
|
|
const node = LiteGraph.createNode("AEAnimation");
|
|
if (!node) return null;
|
|
const canvas = window.LGraphCanvas && window.LGraphCanvas.active_canvas;
|
|
if (canvas?.convertEventToCanvas) {
|
|
node.pos = canvas.convertEventToCanvas([200, 200]);
|
|
} else {
|
|
node.pos = [200, 200];
|
|
}
|
|
graph.add(node);
|
|
canvas?.setDirty?.(true, true);
|
|
canvas?.selectNode?.(node, false);
|
|
return node;
|
|
} catch (e) {
|
|
console.error("[AE Timeline] Failed to create AEAnimation node", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function ensureAEAnimationNode() {
|
|
const graph = getGraph();
|
|
let node = getSelectedAEAnimationNode();
|
|
if (!node) node = findExistingAEAnimation(graph);
|
|
if (!node) node = createAEAnimationNode();
|
|
return node;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "ComfyUI.AEAnimation.TimelineExt",
|
|
// Hide advanced widgets on the node UI for a cleaner look
|
|
nodeCreated(node) {
|
|
if (!node || node.constructor?.comfyClass !== "AEAnimation") return;
|
|
const hideSet = new Set(["mask_expansion", "mask_feather", "layers_keyframes", "start_frame", "end_frame"]);
|
|
if (!node.widgets) return;
|
|
node.widgets.forEach((w) => {
|
|
if (hideSet.has(w.name)) {
|
|
w.computeSize = () => [0, 0];
|
|
w.draw = () => {};
|
|
w.hidden = true;
|
|
}
|
|
});
|
|
// Reduce height after hiding
|
|
if (typeof node.computeSize === "function") {
|
|
const sz = node.computeSize(node.size);
|
|
if (Array.isArray(sz) && sz.length === 2) {
|
|
node.size[1] = Math.min(node.size[1], sz[1]);
|
|
}
|
|
}
|
|
},
|
|
setup() {
|
|
const comfyAPI = window.comfyAPI;
|
|
const ComfyButton = comfyAPI && comfyAPI.button && comfyAPI.button.ComfyButton;
|
|
if (!ComfyButton || !app.menu || !app.menu.settingsGroup) return;
|
|
|
|
app.menu.settingsGroup.append(
|
|
new ComfyButton({
|
|
icon: "timeline",
|
|
tooltip: "AE Animation Timeline",
|
|
content: "AE Timeline",
|
|
action: async () => {
|
|
const node = ensureAEAnimationNode();
|
|
if (!node) {
|
|
alert("无法创建或获取 AE Animation 节点");
|
|
return;
|
|
}
|
|
await openAETimelineForNode(node);
|
|
},
|
|
}),
|
|
);
|
|
},
|
|
getNodeMenuItems(node) {
|
|
const nodeClass = node && node.constructor && node.constructor.comfyClass;
|
|
if (nodeClass !== "AEAnimation") return [];
|
|
|
|
return [
|
|
null,
|
|
{
|
|
content: "Open AE Timeline",
|
|
callback: () => {
|
|
openAETimelineForNode(node);
|
|
},
|
|
},
|
|
];
|
|
},
|
|
});
|