Add files via upload

This commit is contained in:
AI Lab
2026-10-20 00:24:28 -07:00
committed by GitHub
parent 68e6ff46f4
commit 98aa6c5a2b
5 changed files with 2197 additions and 14 deletions
+275
View File
@@ -0,0 +1,275 @@
/**
* AILab - Collapsible Advanced Options Extension
* Fully compatible with both WebUI 1.0 (LiteGraph) and WebUI 2.0 (Vue Canvas).
*
* This integration script is licensed under the GNU General Public License v3.0 (GPL-3.0).
* If you incorporate or modify this code, please credit AILab as the original source:
* https://github.com/1038lab
*/
import { app } from "/scripts/app.js";
const EXTENSION_NAME = "AILab.RMBG.AdvancedToggle";
const NODE_CONFIG = {
"AILab_Yolo": [
"model",
"mask_count",
"select_mask_index",
"unload_model"
],
"AILab_LoadImageAdvanced": [
"image_path_or_URL",
"image",
"upload"
]
};
const TARGET_NODES = Object.keys(NODE_CONFIG);
function getNodeClass(node) {
return node?.comfyClass || node?.type || "";
}
function isPreviewWidget(w) {
return !!w && (w.name === "$$canvas-image-preview" || w.type === "IMAGE_PREVIEW" || (typeof w.name === "string" && w.name.toLowerCase().includes("preview")));
}
function hideWidget(w) {
if (!w) return;
if (!w._origType) {
w._origType = w.type;
w._origComputeSize = w.computeSize;
}
w.type = "hidden";
w.hidden = true;
w.options = w.options || {};
w.options.hidden = true;
w.computeSize = () => [0, -4];
if (w.element) w.element.style.display = "none";
if (w.inputEl) w.inputEl.style.display = "none";
}
function showWidget(w) {
if (!w) return;
w.type = w._origType || "text";
w.hidden = false;
if (w.options) w.options.hidden = false;
if (w._origComputeSize) {
w.computeSize = w._origComputeSize;
} else {
try { delete w.computeSize; } catch (_) { }
}
if (w.element) w.element.style.display = "";
if (w.inputEl) w.inputEl.style.display = "";
}
function getControlsHeight(node) {
const prev = node.widgets?.find(w => isPreviewWidget(w));
let h = 0;
if (prev) {
const origLayout = prev.computeLayoutSize;
const origComp = prev.computeSize;
prev.computeLayoutSize = () => ({ minHeight: 0, minWidth: 1 });
prev.computeSize = () => [0, -4];
h = node.computeSize()[1];
if (origLayout) prev.computeLayoutSize = origLayout;
else delete prev.computeLayoutSize;
if (origComp) prev.computeSize = origComp;
else delete prev.computeSize;
} else {
h = node.computeSize()[1];
}
return h;
}
function applyVisibility(node) {
if (!node || !node.widgets) return;
const cls = getNodeClass(node);
const primaryNames = NODE_CONFIG[cls] || [];
const show = !!node._showAdvanced;
const btn = node.widgets.find(w => w._isAdvBtn);
if (btn) {
const btnIdx = node.widgets.indexOf(btn);
if (btnIdx !== -1) {
node.widgets.splice(btnIdx, 1);
const previewIdx = node.widgets.findIndex(w => isPreviewWidget(w));
if (previewIdx !== -1) {
node.widgets.splice(previewIdx, 0, btn);
} else {
node.widgets.push(btn);
}
}
}
for (const w of node.widgets) {
if (w._isAdvBtn || isPreviewWidget(w)) continue;
if (primaryNames.includes(w.name) || show) {
showWidget(w);
} else {
hideWidget(w);
}
}
const prevWidget = node.widgets.find(w => isPreviewWidget(w));
if (prevWidget) delete prevWidget.computeSize;
const minWidth = node.computeSize()[0] || 280;
const currentWidth = (node.size && node.size[0] > 0) ? node.size[0] : 0;
const targetWidth = Math.max(currentWidth, minWidth);
const controlsHeight = getControlsHeight(node);
let previewHeight = 0;
if (node.imgs && node.imgs.length) {
const img = node.imgs[node.imageIndex || 0] || node.imgs[0];
if (img) {
if (img.naturalWidth && img.naturalHeight) {
const scale = Math.min(targetWidth / img.naturalWidth, 1);
previewHeight = Math.round(img.naturalHeight * scale) + 15;
} else {
previewHeight = 220;
if (!img._advHooked) {
img._advHooked = true;
const origOnload = img.onload;
img.onload = function () {
if (origOnload) origOnload.apply(this, arguments);
applyVisibility(node);
};
}
}
}
}
if (prevWidget && previewHeight > 0) {
prevWidget.computeLayoutSize = () => ({
minHeight: previewHeight,
prefHeight: previewHeight,
minWidth: 1
});
}
node.setSize([targetWidth, controlsHeight + previewHeight]);
node.setDirtyCanvas(true, true);
app.graph?.setDirtyCanvas(true, true);
}
function setupNode(node) {
if (node._advToggleInstalled) return;
const cls = getNodeClass(node);
if (!TARGET_NODES.includes(cls)) return;
if (!node.widgets || node.widgets.length === 0) {
setTimeout(() => setupNode(node), 30);
return;
}
node._advToggleInstalled = true;
node._showAdvanced = false;
const imgWidget = node.widgets.find(w => w.name === "image");
if (imgWidget && !imgWidget._advHooked) {
imgWidget._advHooked = true;
const origCallback = imgWidget.callback;
imgWidget.callback = function () {
const res = origCallback ? origCallback.apply(this, arguments) : undefined;
setTimeout(() => applyVisibility(node), 60);
return res;
};
}
const btn = node.addWidget("button", "Advanced", null, () => {
node._showAdvanced = !node._showAdvanced;
applyVisibility(node);
});
btn._isAdvBtn = true;
btn.serialize = false;
btn.computeSize = () => [0, 26];
Object.defineProperty(btn, "width", {
get() { return undefined; },
set() {}
});
btn.draw = function (ctx, n, widgetWidth, y, widgetHeight) {
const margin = 15;
const x = margin;
const w = widgetWidth - margin * 2;
const h = widgetHeight || 24;
const radius = 8;
const isClicked = !!this.clicked;
this.clicked = false;
ctx.save();
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(x, y, w, h, radius);
} else {
ctx.moveTo(x + radius, y);
ctx.arcTo(x + w, y, x + w, y + h, radius);
ctx.arcTo(x + w, y + h, x, y + h, radius);
ctx.arcTo(x, y + h, x, y, radius);
ctx.arcTo(x, y, x + w, y, radius);
}
ctx.closePath();
ctx.fillStyle = isClicked ? "#282828" : "#181818";
ctx.fill();
ctx.strokeStyle = isClicked ? "rgba(255, 255, 255, 0.22)" : "rgba(255, 255, 255, 0.1)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = isClicked ? "#ffffff" : "#8e9297";
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Advanced", x + w / 2, y + h / 2);
ctx.restore();
if (isClicked) {
n?.setDirtyCanvas?.(true);
}
};
const origOnDrawForeground = node.onDrawForeground;
node.onDrawForeground = function (ctx) {
const b = this.widgets?.find(w => w._isAdvBtn);
if (b) {
const bIdx = this.widgets.indexOf(b);
if (this.widgets.slice(bIdx + 1).some(w => !isPreviewWidget(w))) {
applyVisibility(this);
}
}
return origOnDrawForeground ? origOnDrawForeground.apply(this, arguments) : undefined;
};
setTimeout(() => {
applyVisibility(node);
}, 30);
}
app.registerExtension({
name: EXTENSION_NAME,
async setup() {
if (app.graph?._nodes) {
for (const node of app.graph._nodes) {
if (TARGET_NODES.includes(getNodeClass(node))) {
setupNode(node);
}
}
}
},
async nodeCreated(node) {
if (TARGET_NODES.includes(getNodeClass(node))) {
setTimeout(() => setupNode(node), 20);
}
},
async loadedGraphNode(node) {
if (TARGET_NODES.includes(getNodeClass(node))) {
setTimeout(() => setupNode(node), 20);
}
}
});
+1502
View File
File diff suppressed because it is too large Load Diff
+76
View File
@@ -3,6 +3,7 @@ import { app } from "/scripts/app.js";
const COLOR_THEMES = {
segment: { nodeColor: "#222e40", nodeBgColor: "#364254", width: 340 },
utility: { nodeColor: "#2e3e57", nodeBgColor: "#4b5b73", width: 300 },
canvas: { nodeColor: "#222e40", nodeBgColor: "#364254", width: 640 },
};
const NODE_COLORS = {
@@ -22,6 +23,10 @@ const NODE_COLORS = {
"SAM2SegmentDiscoveryAdv": "segment",
"SAM3Segment": "segment",
"AILab_Florence2": "segment",
"AILab_Yolo": "segment",
"AILab_VRMBG": "segment",
"AILab_FeyNoBg": "segment",
"AILab_FinegrainBoxSegment": "segment",
// Utility nodes
"AILab_LoadImage": "utility",
@@ -54,6 +59,7 @@ const NODE_COLORS = {
"AILab_ColorToMask": "utility",
"AILab_Resolution_Selector": "utility",
"AILab_ImageCompareView": "utility",
"AILab_Canvas": "canvas",
};
function setNodeColors(node, theme) {
@@ -83,4 +89,74 @@ const ext = {
}
};
export const SLOT_AFFINITY = {
rules: {
input: {
IMAGE: [
"AILab_ImagePreview",
"AILab_Preview",
"AILab_ImageCompareView",
"RMBG"
],
MASK: [
"AILab_MaskPreview",
"AILab_Preview",
"AILab_MaskOverlay"
],
BBOX: [
"AILab_FinegrainBoxSegment"
],
BBOXES: [
"AILab_FinegrainBoxSegment"
]
},
output: {
IMAGE: [
"AILab_LoadImage",
"AILab_LoadImageSimple",
"AILab_LoadImageAdvanced"
],
MASK: [
"RMBG",
"AILab_ColorToMask",
"SAM3Segment"
],
BBOX: [
"AILab_Florence2",
"SAM3Segment"
],
BBOXES: [
"AILab_Florence2",
"SAM3Segment"
]
}
},
metrics: {
"AILab_Preview": 4600,
"AILab_ImagePreview": 4500,
"AILab_LoadImageSimple": 4500,
"RMBG": 4500,
"AILab_LoadImage": 4400,
"AILab_LoadImageAdvanced": 4300,
"AILab_ColorToMask": 4200,
"SAM3Segment": 4200,
"AILab_Florence2": 4200,
"AILab_FinegrainBoxSegment": 4200
},
defaultSlots: {
out: {
IMAGE: ["AILab_ImagePreview"],
MASK: ["AILab_MaskPreview"],
BBOX: ["AILab_FinegrainBoxSegment"],
BBOXES: ["AILab_FinegrainBoxSegment"]
},
in: {
IMAGE: ["AILab_LoadImage"],
MASK: ["RMBG"],
BBOX: ["AILab_Florence2", "SAM3Segment"],
BBOXES: ["AILab_Florence2", "SAM3Segment"]
}
}
};
app.registerExtension(ext);
+59 -14
View File
@@ -3,7 +3,7 @@
*
* This integration script is licensed under the GNU General Public License v3.0 (GPL-3.0).
* If you incorporate or modify this code, please credit AILab as the original source:
* https://github.com/1038lab
* https://github.com/1038lab/ComfyUI-RMBG
*/
import { app } from "/scripts/app.js";
@@ -59,12 +59,12 @@ function hsvToRGB(h, s, v) {
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
else { r = c; g = 0; b = x; }
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
@@ -111,6 +111,8 @@ function openColorPicker(initialColor, clientX, clientY, onApply) {
panel.className = 'ailab-cp-panel';
Object.assign(panel.style, {
position: 'fixed', zIndex: '99999',
width: `${SV_SIZE + PAD * 2}px`,
boxSizing: 'border-box',
background: '#1e1e1e', border: '1px solid #444',
borderRadius: '8px', padding: `${PAD}px`,
boxShadow: '0 8px 32px rgba(0,0,0,0.6)',
@@ -136,36 +138,79 @@ function openColorPicker(initialColor, clientX, clientY, onApply) {
marginBottom: `${GAP}px`,
});
// Bottom row: preview swatch + hex input + OK button
// Bottom row: preview swatch + eyedropper + hex input + OK button
const row = document.createElement('div');
Object.assign(row.style, {
display: 'flex', alignItems: 'center', gap: `${GAP}px`,
display: 'flex', alignItems: 'center', gap: '6px',
width: '100%', boxSizing: 'border-box',
});
const preview = document.createElement('div');
Object.assign(preview.style, {
width: '28px', height: '28px', borderRadius: '4px',
border: '1px solid #555', flexShrink: '0',
width: '26px', height: '26px', borderRadius: '4px',
border: '1px solid #555', flexShrink: '0', boxSizing: 'border-box',
});
let dropBtn = null;
if (window.EyeDropper) {
dropBtn = document.createElement('button');
dropBtn.type = 'button';
dropBtn.title = 'Pick color from screen';
Object.assign(dropBtn.style, {
width: '26px', height: '26px', borderRadius: '4px',
border: '1px solid #555', background: '#333', color: '#ccc',
flexShrink: '0', cursor: 'pointer', padding: '0',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxSizing: 'border-box',
transition: 'background 0.15s, border-color 0.15s, color 0.15s',
});
dropBtn.innerHTML = `<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" style="display:block;pointer-events:none;"><path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.708L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708zM2 12.707l7-7L10.293 7l-7 7H2z"/></svg>`;
dropBtn.addEventListener('mouseenter', () => {
dropBtn.style.background = '#444';
dropBtn.style.borderColor = '#777';
dropBtn.style.color = '#fff';
});
dropBtn.addEventListener('mouseleave', () => {
dropBtn.style.background = '#333';
dropBtn.style.borderColor = '#555';
dropBtn.style.color = '#ccc';
});
dropBtn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
const eyeDropper = new window.EyeDropper();
const result = await eyeDropper.open();
if (result && result.sRGBHex) {
hsv = hexToHSV(result.sRGBHex);
updateAll();
}
} catch (err) {}
});
}
const hexInput = document.createElement('input');
hexInput.type = 'text';
Object.assign(hexInput.style, {
flex: '1', background: '#333', border: '1px solid #555',
borderRadius: '4px', color: '#eee', padding: '4px 8px',
fontFamily: 'monospace', fontSize: '13px', outline: 'none',
width: '80px', minWidth: '0',
flex: '1', width: '0', minWidth: '0',
height: '26px', boxSizing: 'border-box',
background: '#333', border: '1px solid #555',
borderRadius: '4px', color: '#eee', padding: '0 4px',
fontFamily: 'monospace', fontSize: '11px', outline: 'none',
textAlign: 'center',
});
const okBtn = document.createElement('button');
okBtn.textContent = 'OK';
Object.assign(okBtn.style, {
height: '26px', boxSizing: 'border-box',
background: '#4a9eff', color: '#fff', border: 'none',
borderRadius: '4px', padding: '5px 14px', cursor: 'pointer',
fontSize: '12px', fontWeight: 'bold',
borderRadius: '4px', padding: '0 10px', cursor: 'pointer',
fontSize: '11px', fontWeight: 'bold', flexShrink: '0',
});
row.append(preview, hexInput, okBtn);
row.append(preview);
if (dropBtn) row.append(dropBtn);
row.append(hexInput, okBtn);
panel.append(svCanvas, hueCanvas, row);
// ── Position panel below click point, clamped to viewport ──
+285
View File
@@ -0,0 +1,285 @@
import { app } from "/scripts/app.js";
import { SLOT_AFFINITY } from "./appearance.js";
const EXTENSION_NAME = "AILab.WorkflowUtils";
export function clamp(val, min, max) {
return Math.min(Math.max(val, min), max);
}
export function debounce(fn, delay = 200) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
export function hexToRgb(hex) {
if (!hex) return [0, 0, 0];
const clean = hex.replace("#", "");
const bigint = parseInt(clean, 16);
if (clean.length === 6) {
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
}
return [0, 0, 0];
}
const g = typeof window !== "undefined" ? window : globalThis;
g.__AILAB_WORKFLOW_UTILS__ = g.__AILAB_WORKFLOW_UTILS__ || {
configs: [],
register(cfg) {
if (cfg) this.configs.push(cfg);
},
getMergedRules() {
const merged = { input: {}, output: {} };
for (const c of this.configs) {
for (const dir of ["input", "output"]) {
const rules = c.rules?.[dir] || {};
for (const [slot, list] of Object.entries(rules)) {
merged[dir][slot] = merged[dir][slot] || [];
for (const name of list) {
if (!merged[dir][slot].includes(name)) {
merged[dir][slot].push(name);
}
}
}
}
}
return merged;
},
getMergedMetrics() {
const merged = {};
for (const c of this.configs) {
Object.assign(merged, c.metrics || {});
}
return merged;
},
isMatchingNode(node) {
if (!node) return false;
const name = node.name || node.type || node.comfyClass || "";
if (name.startsWith("AILab_") || name === "RMBG" || name.startsWith("SAM")) return true;
const category = node.category || "";
if (category.includes("AILab") || category.includes("RMBG")) return true;
const displayName = node.display_name || "";
return displayName.includes("(RMBG)") || displayName.includes("AILab");
}
};
if (SLOT_AFFINITY) {
g.__AILAB_WORKFLOW_UTILS__.register(SLOT_AFFINITY);
}
if (typeof window !== "undefined" && window.fetch && !g.__AILAB_WORKFLOW_UTILS__._fetchHooked) {
const origFetch = window.fetch;
window.fetch = async function (...args) {
const res = await origFetch.apply(this, args);
const url = typeof args[0] === "string" ? args[0] : (args[0]?.url || "");
if (url.includes("sorted-custom-node-map.json")) {
try {
const clone = res.clone();
const data = await clone.json();
Object.assign(data, g.__AILAB_WORKFLOW_UTILS__.getMergedMetrics());
return new Response(JSON.stringify(data), {
status: res.status,
statusText: res.statusText,
headers: res.headers
});
} catch (_) {
return res;
}
}
return res;
};
g.__AILAB_WORKFLOW_UTILS__._fetchHooked = true;
}
function reorderResults(results, filters, query) {
if (!Array.isArray(results) || results.length === 0) return results;
const trimmed = (query || "").trim().toLowerCase();
const hasSlot = filters && filters.length > 0;
let dir = "";
let slotType = "";
if (hasSlot) {
const f = filters.find(item => {
const fid = item.filterDef?.id || item.id || "";
const seq = item.filterDef?.invokeSequence || "";
return fid === "input" || fid === "output" || seq === "i" || seq === "o";
});
if (f) {
const fid = f.filterDef?.id || f.id || "";
const seq = f.filterDef?.invokeSequence || "";
dir = (fid === "input" || seq === "i") ? "input" : "output";
slotType = String(f.value || "").toUpperCase();
}
}
const mergedRules = g.__AILAB_WORKFLOW_UTILS__.getMergedRules();
if (dir && slotType && (!trimmed || trimmed === "rmbg" || trimmed === "ailab")) {
const priority = mergedRules[dir]?.[slotType] || [];
if (priority.length > 0) {
const pinned = [];
const rest = [];
for (const name of priority) {
const found = results.find(n => (n.name || n.type) === name);
if (found) pinned.push(found);
}
const pinnedSet = new Set(pinned.map(p => p.name || p.type));
for (const item of results) {
const name = item.name || item.type;
if (!pinnedSet.has(name)) {
rest.push(item);
}
}
if (dir === "output") {
const previewSet = new Set(["AILab_ImagePreview", "AILab_Preview", "AILab_MaskPreview"]);
const normal = [];
const previews = [];
for (const item of rest) {
if (previewSet.has(item.name || item.type)) {
previews.push(item);
} else {
normal.push(item);
}
}
return [...pinned, ...normal, ...previews];
}
if (pinned.length > 0) {
return [...pinned, ...rest];
}
}
}
if (trimmed && (trimmed === "rmbg" || trimmed === "sam" || trimmed === "ailab")) {
const matched = [];
const normal = [];
for (const item of results) {
if (g.__AILAB_WORKFLOW_UTILS__.isMatchingNode(item)) {
matched.push(item);
} else {
normal.push(item);
}
}
if (dir && slotType && matched.length > 0) {
const priority = mergedRules[dir]?.[slotType] || [];
matched.sort((a, b) => {
const aIdx = priority.indexOf(a.name || a.type || "");
const bIdx = priority.indexOf(b.name || b.type || "");
return (aIdx !== -1 ? aIdx : 999) - (bIdx !== -1 ? bIdx : 999);
});
}
return [...matched, ...normal];
}
return results;
}
function hookService(service) {
if (!service || service._ailabHooked) return;
const ServiceClass = service.constructor;
const proto = (ServiceClass && ServiceClass.prototype && typeof ServiceClass.prototype.searchNode === "function")
? ServiceClass.prototype
: service;
const origSearch = proto.searchNode;
if (typeof origSearch !== "function") return;
proto.searchNode = function (query, filters = [], options = {}, extra = {}) {
const origLimit = options?.limit || 64;
const opt = { ...options };
if (opt.limit) delete opt.limit;
const full = origSearch.call(this, query, filters, opt, extra);
const reordered = reorderResults(full, filters, query);
return reordered.slice(0, origLimit);
};
service._ailabHooked = true;
if (ServiceClass && ServiceClass.prototype) {
ServiceClass.prototype._ailabHooked = true;
}
}
function connectService() {
try {
const vue = document.getElementById("vue-app")?.__vue_app__;
let pinia = vue?.config?.globalProperties?.$pinia;
if (!pinia && vue?._context?.provides) {
for (const sym of Object.getOwnPropertySymbols(vue._context.provides)) {
const val = vue._context.provides[sym];
if (val && typeof val === "object" && val._s instanceof Map) {
pinia = val;
break;
}
}
}
if (pinia && pinia._s instanceof Map) {
const store = pinia._s.get("nodeDef");
if (store?.nodeSearchService) {
hookService(store.nodeSearchService);
return true;
}
}
} catch (_) { }
return false;
}
function initLiteGraphDefaults() {
if (typeof LiteGraph === "undefined") return;
LiteGraph.slot_types_default_out = LiteGraph.slot_types_default_out || {};
LiteGraph.slot_types_default_in = LiteGraph.slot_types_default_in || {};
for (const c of g.__AILAB_WORKFLOW_UTILS__.configs) {
if (c.defaultSlots?.out) {
for (const [slot, list] of Object.entries(c.defaultSlots.out)) {
const cur = LiteGraph.slot_types_default_out[slot] || [];
const add = list.filter(n => !cur.includes(n));
if (add.length > 0) {
LiteGraph.slot_types_default_out[slot] = [...add, ...cur];
}
}
}
if (c.defaultSlots?.in) {
for (const [slot, list] of Object.entries(c.defaultSlots.in)) {
const cur = LiteGraph.slot_types_default_in[slot] || [];
const add = list.filter(n => !cur.includes(n));
if (add.length > 0) {
LiteGraph.slot_types_default_in[slot] = [...add, ...cur];
}
}
}
}
}
app.registerExtension({
name: EXTENSION_NAME,
async setup() {
initLiteGraphDefaults();
let attempts = 0;
const timer = setInterval(() => {
attempts++;
if (connectService() || attempts > 25) {
clearInterval(timer);
}
}, 200);
window.addEventListener("pointerdown", () => connectService(), { passive: true });
}
});