Add files via upload
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user