200 lines
7.1 KiB
JavaScript
200 lines
7.1 KiB
JavaScript
// LinkModeToggle — front-end extension for ComfyUI
|
||
// Adds: F8/Ctrl+K hotkeys + a small toolbar button that cycles SPLINE/LINEAR/STRAIGHT.
|
||
|
||
(() => {
|
||
const ORDER = ["SPLINE","LINEAR","STRAIGHT"];
|
||
const KEY = "LinkModeToggle.mode";
|
||
|
||
// Robustly get the LiteGraph canvas object and the DOM <canvas> elements
|
||
const getCanvasObj = () => window.app?.canvas || window.app?.graphcanvas || null;
|
||
|
||
// Cycle & persistence
|
||
const setMode = (name) => {
|
||
const c = getCanvasObj();
|
||
if (!c) return false;
|
||
const modes = { STRAIGHT:0, SPLINE:1, LINEAR:2 };
|
||
|
||
const ok =
|
||
(typeof c.setLinkRenderMode === "function" && (c.setLinkRenderMode(modes[name]), true)) ||
|
||
("links_render_mode" in c && (() => {
|
||
const K = c.constructor;
|
||
const KM = {
|
||
STRAIGHT: K?.LINK_RENDER_MODE_STRAIGHT ?? K?.STRAIGHT_LINKS ?? 0,
|
||
SPLINE: K?.LINK_RENDER_MODE_SPLINE ?? K?.SPLINE_LINKS ?? 1,
|
||
LINEAR: K?.LINK_RENDER_MODE_LINEAR ?? K?.LINEAR_LINKS ?? 2,
|
||
};
|
||
c.links_render_mode = KM[name] ?? modes[name];
|
||
c.dirty_canvas = true; c.draw(true, true);
|
||
return true;
|
||
})()) ||
|
||
("render_curved_links" in c && (() => {
|
||
c.render_curved_links = (name === "SPLINE");
|
||
c.dirty_canvas = true; c.draw(true, true);
|
||
return true;
|
||
})()) ||
|
||
("render_link_curved" in c && (() => {
|
||
c.render_link_curved = (name === "SPLINE");
|
||
c.redraw?.(true,true); c.draw?.(true,true);
|
||
return true;
|
||
})());
|
||
|
||
if (ok) localStorage.setItem(KEY, name);
|
||
updateButtonUI(name, ok);
|
||
return ok;
|
||
};
|
||
|
||
const cycle = () => {
|
||
const cur = localStorage.getItem(KEY) || "SPLINE";
|
||
const idx = Math.max(0, ORDER.indexOf(cur));
|
||
const next = ORDER[(idx + 1) % ORDER.length];
|
||
setMode(next);
|
||
};
|
||
|
||
// Button
|
||
let btn, badge;
|
||
const updateButtonUI = (mode, ok) => {
|
||
if (!btn) return;
|
||
btn.dataset.mode = mode;
|
||
btn.title = `Link Mode: ${mode} (F8 / Ctrl+K to toggle)`;
|
||
if (badge) badge.textContent = mode[0]; // S/L/S
|
||
btn.classList.toggle("lmt-failed", !ok);
|
||
};
|
||
|
||
// --- build a Prime/Comfy-styled button like the others in the toolbar ---
|
||
const buildButton = () => {
|
||
if (btn) return btn;
|
||
|
||
btn = document.createElement("button");
|
||
// match existing toolbar buttons
|
||
btn.className = [
|
||
"p-button", "p-component", "p-button-icon-only", "p-button-secondary",
|
||
"h-8", "w-8", "bg-interface-panel-surface", "p-0", "hover:bg-button-hover-surface!"
|
||
].join(" ");
|
||
btn.type = "button";
|
||
btn.setAttribute("aria-label", "Cycle Link Mode (F8 / Ctrl+K)");
|
||
btn.setAttribute("data-pc-name", "button");
|
||
btn.setAttribute("data-p-disabled", "false");
|
||
btn.setAttribute("data-p-severity", "secondary");
|
||
btn.style.borderRadius = "8px";
|
||
btn.style.border = "none";
|
||
|
||
// icon (uses lucide class system like other buttons)
|
||
const icon = document.createElement("i");
|
||
// looks like a curve; feel free to change to: lucide--bezier-curve / pen-tool / route
|
||
icon.className = "icon-[lucide--bezier-curve] h-4 w-4";
|
||
btn.appendChild(icon);
|
||
|
||
// tiny badge (S/L/…) in the corner so you know current mode at a glance
|
||
badge = document.createElement("span");
|
||
badge.className = "lmt-badge";
|
||
badge.textContent = (localStorage.getItem(KEY) || "SPLINE")[0];
|
||
btn.appendChild(badge);
|
||
|
||
btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); cycle(); });
|
||
return btn;
|
||
};
|
||
|
||
|
||
// --- find the bottom-right group and dock the button there ---
|
||
const attachButton = () => {
|
||
const b = buildButton();
|
||
|
||
// Find the toolbar group you pasted (bottom-right palette)
|
||
// We anchor off a known child button to be resilient to class churn.
|
||
const group =
|
||
document.querySelector('[data-testid="toggle-link-visibility-button"]')
|
||
?.closest('span.p-buttongroup[role="group"]')
|
||
|| document.querySelector('span.p-buttongroup[role="group"].right-0.bottom-0');
|
||
|
||
if (group) {
|
||
// Optional: put a divider before our button for symmetry
|
||
const divider = document.createElement("div");
|
||
divider.className = "h-[27px] w-[1px] self-center bg-node-divider";
|
||
|
||
// If we don’t already exist, append divider + button to the group
|
||
if (!group.contains(b)) {
|
||
group.appendChild(divider);
|
||
group.appendChild(b);
|
||
}
|
||
b.classList.remove("lmt-floating", "lmt-bottom-right", "lmt-in-toolbar");
|
||
// We’re in the toolbar now
|
||
return true;
|
||
}
|
||
|
||
// Fallback: float at bottom-right (won’t block nodes; z-index is high)
|
||
if (!document.body.contains(b)) document.body.appendChild(b);
|
||
b.classList.add("lmt-floating", "lmt-bottom-right");
|
||
return false;
|
||
};
|
||
|
||
|
||
// Keep hotkeys the same; just add a MutationObserver so we reattach if UI changes
|
||
const observeUI = () => {
|
||
const obs = new MutationObserver(() => {
|
||
// if button got removed (UI re-render), re-attach
|
||
if (!document.querySelector(".lmt-btn")) attachButton();
|
||
});
|
||
obs.observe(document.body, { childList: true, subtree: true });
|
||
return obs;
|
||
};
|
||
|
||
|
||
|
||
|
||
// Hotkeys
|
||
const keyHandler = (e) => {
|
||
const ctrlK = e.code === "KeyK" && e.ctrlKey && !e.altKey && !e.shiftKey;
|
||
const f8 = e.code === "F8" && !e.ctrlKey && !e.altKey && !e.shiftKey;
|
||
if (!(ctrlK || f8)) return;
|
||
|
||
const t = e.target;
|
||
const tag = (t && t.tagName) || "";
|
||
if (tag === "INPUT" || tag === "TEXTAREA" || t?.isContentEditable) return;
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
cycle();
|
||
};
|
||
|
||
const bindHotkeys = () => {
|
||
const cobj = getCanvasObj();
|
||
const targets = [window, document, document.body, cobj?.canvas, cobj?.bgcanvas]
|
||
.filter(t => t && typeof t.addEventListener === "function");
|
||
targets.forEach(t => t.addEventListener("keydown", keyHandler, { capture: true }));
|
||
console.log("[LinkModeToggle] Hotkeys ready (F8 / Ctrl+K). Bound on:", targets.length, "targets.");
|
||
};
|
||
|
||
// Boot sequence with retries until app + canvas are ready
|
||
const boot = () => {
|
||
const tryInit = (attempt = 0) => {
|
||
const ready = window.app && getCanvasObj();
|
||
if (!ready) {
|
||
if (attempt < 60) return void setTimeout(() => tryInit(attempt + 1), 250);
|
||
console.warn("[LinkModeToggle] Timed out waiting for ComfyUI app/canvas.");
|
||
return;
|
||
}
|
||
setMode(localStorage.getItem(KEY) || "SPLINE");
|
||
attachButton();
|
||
bindHotkeys();
|
||
observeUI();
|
||
};
|
||
tryInit();
|
||
};
|
||
|
||
// Use Comfy’s extension API if present
|
||
if (window.app?.registerExtension) {
|
||
app.registerExtension({
|
||
name: "LinkModeToggle",
|
||
setup() { boot(); },
|
||
});
|
||
} else {
|
||
// Older frontends: best-effort boot
|
||
document.addEventListener("DOMContentLoaded", boot);
|
||
boot();
|
||
}
|
||
})();
|
||
|
||
|
||
console.log("%c[LinkModeToggle] v0.1.0 loaded","color: #00e6e6;");
|
||
|