Files

495 lines
19 KiB
JavaScript

import { app } from "../../../scripts/app.js";
// --- BSS Premium UI: Luxury Matte & Smart Widgets (v1.3.0) ---
// Создает изысканный, минималистичный дизайн в стиле премиальной аудиоаппаратуры для FLSampler.
// 100% корректное отзывчивое управление слайдерами (полоса на всю ширину по низу виджета).
// Никаких лишних разделителей в бэкграунде. 14px "воздуха" внизу ноды для гравировки.
// Инъекция глобальных стилей для мгновенного скрытия тултипов во время перетаскивания и регулировки
const style = document.createElement("style");
style.textContent = `
.bss-dragging-active .comfy-tooltip,
.bss-dragging-active .comfy-help-tooltip,
.bss-dragging-active .litegraph-tooltip,
.bss-dragging-active [tooltip] {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
`;
document.head.appendChild(style);
// Отслеживание клика мыши по всему окну для перетаскивания
window.addEventListener("pointerdown", () => {
document.body.classList.add("bss-dragging-active");
}, { capture: true, passive: true });
window.addEventListener("pointerup", () => {
document.body.classList.remove("bss-dragging-active");
}, { capture: true, passive: true });
window.addEventListener("pointercancel", () => {
document.body.classList.remove("bss-dragging-active");
}, { capture: true, passive: true });
// Функция для полной блокировки и скрытия тултипов у виджета
function setupPremiumTooltipHandler(widget) {
if (widget.tooltip !== undefined) {
widget.tooltip = null;
}
Object.defineProperty(widget, "tooltip", {
get() {
return null;
},
set(val) {},
configurable: true,
enumerable: true
});
}
// Функция для полного удаления всех тултипов у ноды (виджеты, входы, выходы)
function removeAllNodeTooltips(node) {
if (node.widgets) {
node.widgets.forEach(w => {
setupPremiumTooltipHandler(w);
});
}
if (node.inputs) {
node.inputs.forEach(input => {
input.tooltip = null;
});
}
if (node.outputs) {
node.outputs.forEach(output => {
output.tooltip = null;
});
}
}
// Функция для форматирования числовых значений с правильной точностью
function formatDisplayValue(value, widget) {
if (typeof value !== "number") return value;
const name = String(widget?.name || "").toLowerCase();
// Бронебойная проверка: для всех ключевых параметров гарантируем вывод сотых долей
if (
name.includes("threshold") ||
name.includes("factor") ||
name.includes("percent") ||
name.includes("scale") ||
name.includes("denoise") ||
name.includes("cfg") ||
name.includes("sharpness") ||
name.includes("inertia") ||
(widget?.options?.step !== undefined && widget.options.step < 0.1)
) {
return value.toFixed(2);
}
// Для остальных параметров определяем точность по шагу
const step = widget?.options?.step;
if (typeof step === "number" && step > 0) {
const stepStr = String(step);
const dotIdx = stepStr.indexOf(".");
if (dotIdx !== -1) {
const precision = stepStr.length - dotIdx - 1;
return value.toFixed(precision);
}
}
// Если число не целое, выводим 2 знака по умолчанию
if (!Number.isInteger(value)) {
return value.toFixed(2);
}
return String(value);
}
// Применение премиум стилей к ноде
function applyBssLuxuryStyles(node) {
node.bgcolor = "#0f0f0eff"; // Глубокий ультра-темный матовый графит
node.color = "#0f0f0eff"; // Синхронизируем с bgcolor: убирает дублирующий темный шейп разъемов
node.boxcolor = "rgba(212, 175, 55, 0.6)"; // Благородный золотой контур активной ноды (#d4af37)
node.rounded = true; // Аккуратные скругления углов
node.title_font = "bold 11px 'Segoe UI', system-ui, sans-serif";
}
// --- КАСТОМНЫЕ РЕНДЕРЕРЫ ДЛЯ ВЕКТОРНЫХ ВИДЖЕТОВ ---
// Слайдер
function drawPremiumSlider(ctx, node, widget_width, y, margin, widget) {
const height = widget.height || 20;
const value = widget.value;
const min = widget.options.min ?? 0;
const max = widget.options.max ?? 100;
const range = max - min;
const ratio = Math.max(0, Math.min(1, (value - min) / range));
const x = margin;
const slider_w = widget_width - margin * 2;
const slider_y = y + height - 2;
ctx.save();
// 1. Имя параметра слева вверху
ctx.font = "10px 'Segoe UI', system-ui, sans-serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
ctx.textAlign = "left";
ctx.fillText(widget.label || widget.name, x, y + 11);
// 2. Числовое значение справа вверху
ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
ctx.textAlign = "right";
const displayValue = formatDisplayValue(value, widget);
ctx.fillText(displayValue, x + slider_w, y + 11);
// 3. Тонкая подложка слайдера
ctx.strokeStyle = "rgba(255, 255, 255, 0.08)";
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(x, slider_y);
ctx.lineTo(x + slider_w, slider_y);
ctx.stroke();
// 4. Активная золотистая часть
const active_x = x + ratio * slider_w;
ctx.strokeStyle = "#d4af37"; // Яркое благородное золото
ctx.beginPath();
ctx.moveTo(x, slider_y);
ctx.lineTo(active_x, slider_y);
ctx.stroke();
// 5. Круглый золотой бегунок-кноб
ctx.fillStyle = "#e5c158";
ctx.beginPath();
ctx.arc(active_x, slider_y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Тоггл
function drawPremiumToggle(ctx, node, widget_width, y, margin, widget) {
const height = widget.height || 20;
const value = !!widget.value;
const x = margin;
const toggle_w = 24;
const toggle_h = 12;
const toggle_x = widget_width - margin - toggle_w;
const toggle_y = y + (height - toggle_h) / 2;
ctx.save();
// 1. Имя параметра
ctx.font = "10px 'Segoe UI', system-ui, sans-serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
ctx.textAlign = "left";
ctx.fillText(widget.label || widget.name, x, y + 13);
// 2. Овальный корпус тоггла
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(toggle_x, toggle_y, toggle_w, toggle_h, 6);
} else {
ctx.rect(toggle_x, toggle_y, toggle_w, toggle_h);
}
if (value) {
ctx.fillStyle = "rgba(212, 175, 55, 0.85)";
} else {
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
}
ctx.fill();
// 3. Круглый белый бегунок
const knob_r = 4;
const knob_y = toggle_y + toggle_h / 2;
const knob_x = value ? (toggle_x + toggle_w - knob_r - 2) : (toggle_x + knob_r + 2);
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(knob_x, knob_y, knob_r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Комбо-бокс (Выпадающий список)
function drawPremiumCombo(ctx, node, widget_width, y, margin, widget) {
const height = widget.height || 20;
const value = widget.value;
const x = margin;
const combo_w = widget_width - margin * 2;
const combo_h = height - 4;
const combo_y = y + 2;
ctx.save();
// 1. Рамка комбо-бокса
ctx.fillStyle = "rgba(255, 255, 255, 0.03)";
ctx.strokeStyle = "rgba(212, 175, 55, 0.35)"; // Золотистый контур
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(x, combo_y, combo_w, combo_h, 4);
} else {
ctx.rect(x, combo_y, combo_w, combo_h);
}
ctx.fill();
ctx.stroke();
// 2. Название параметра
ctx.font = "10px 'Segoe UI', system-ui, sans-serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.55)";
const labelText = (widget.label || widget.name) + ":";
ctx.fillText(labelText, x + 8, combo_y + 11);
// 3. Значение
const label_w = ctx.measureText(labelText).width;
ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
let valText = String(value);
const max_val_w = combo_w - label_w - 28;
if (ctx.measureText(valText).width > max_val_w) {
while (valText.length > 0 && ctx.measureText(valText + "...").width > max_val_w) {
valText = valText.substring(0, valText.length - 1);
}
valText += "...";
}
ctx.fillText(valText, x + label_w + 12, combo_y + 11);
// 4. Стрелочка
ctx.fillStyle = "rgba(212, 175, 55, 0.85)"; // Золотая стрелочка
ctx.beginPath();
const arrow_x = x + combo_w - 10;
const arrow_y = combo_y + 7;
ctx.moveTo(arrow_x - 3, arrow_y - 1);
ctx.lineTo(arrow_x + 3, arrow_y - 1);
ctx.lineTo(arrow_x, arrow_y + 2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Числовой инпут
function drawPremiumNumber(ctx, node, widget_width, y, margin, widget) {
const height = widget.height || 20;
const value = widget.value;
const x = margin;
const box_w = widget_width - margin * 2;
const box_h = height - 4;
const box_y = y + 2;
ctx.save();
// 1. Рамка
ctx.fillStyle = "rgba(255, 255, 255, 0.03)";
ctx.strokeStyle = "rgba(212, 175, 55, 0.25)"; // Золотистая рамка инпута
ctx.lineWidth = 1;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(x, box_y, box_w, box_h, 4);
} else {
ctx.rect(x, box_y, box_w, box_h);
}
ctx.fill();
ctx.stroke();
// 2. Имя параметра
ctx.font = "10px 'Segoe UI', system-ui, sans-serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.55)";
const labelText = (widget.label || widget.name) + ":";
ctx.fillText(labelText, x + 8, box_y + 11);
// 3. Значение справа
ctx.fillStyle = "rgba(255, 255, 255, 0.95)";
ctx.textAlign = "right";
ctx.fillText(String(value), x + box_w - 8, box_y + 11);
ctx.restore();
}
// Применение кастомных отрисовщиков
function applyPremiumWidgetRenderers(node) {
if (!node.widgets) return;
removeAllNodeTooltips(node);
node.widgets.forEach(widget => {
const type = widget.type === "hidden" ? widget._original_type : widget.type;
const lowerType = String(type || "").toLowerCase();
const name = String(widget.name || "").toLowerCase();
// Принудительно корректируем опции, чтобы обойти кэширование старого workflow JSON
if (name === "fovea_strength") {
widget.options = widget.options || {};
widget.options.min = 0.0;
widget.options.max = 10.0;
widget.options.step = 0.1;
} else if (name === "sharpness") {
widget.options = widget.options || {};
widget.options.min = 0.0;
widget.options.max = 3.0;
widget.options.step = 0.05;
} else if (name === "mask_inertia") {
widget.options = widget.options || {};
widget.options.min = 0.0;
widget.options.max = 0.99;
widget.options.step = 0.01;
} else if (name === "cfg") {
widget.options = widget.options || {};
widget.options.step = 0.1;
}
if (name === "fovea_strength" || name === "sharpness" || name === "mask_inertia") {
widget.draw = function (ctx, node, widget_width, y, margin) {
drawPremiumSlider(ctx, node, widget_width, y, margin, this);
};
// 100% корректное физическое перетаскивание по нашему премиальному слайдеру с округлением шага
widget.mouse = function (e, pos, node) {
if (e.type === "mousedown" || e.type === "pointerdown" || e.type === "mousemove" || e.type === "pointermove") {
const min = this.options.min ?? 0;
const max = this.options.max ?? 100;
const step = this.options.step ?? 0.1;
const margin = 10;
const widget_width = node.size[0];
const slider_x_start = margin;
const slider_w = widget_width - margin * 2;
let ratio = (pos[0] - slider_x_start) / slider_w;
ratio = Math.max(0, Math.min(1, ratio));
let val = min + ratio * (max - min);
if (step > 0) {
val = Math.round(val / step) * step;
// Исправляем JavaScript артефакты точности с плавающей точкой
const stepStr = String(step);
const dotIdx = stepStr.indexOf(".");
if (dotIdx !== -1) {
const precision = stepStr.length - dotIdx - 1;
val = parseFloat(val.toFixed(precision));
}
}
val = Math.max(min, Math.min(max, val));
this.value = val;
if (this.callback) {
this.callback(this.value, node);
}
node.setDirtyCanvas(true, true);
return true;
}
return false;
};
} else if (lowerType === "toggle" || typeof widget.value === "boolean") {
widget.draw = function (ctx, node, widget_width, y, margin) {
drawPremiumToggle(ctx, node, widget_width, y, margin, this);
};
} else if (lowerType === "combo") {
widget.draw = function (ctx, node, widget_width, y, margin) {
drawPremiumCombo(ctx, node, widget_width, y, margin, this);
};
} else if (lowerType === "number" || lowerType === "int" || lowerType === "float" || lowerType === "integer" || widget.options?.display === "number" || name === "steps" || name === "seed" || name === "cfg" || name === "denoise") {
widget.draw = function (ctx, node, widget_width, y, margin) {
drawPremiumNumber(ctx, node, widget_width, y, margin, this);
};
}
});
}
app.registerExtension({
name: "BSS.FLSamplerUI",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "FLS_SamplerV4") {
// --- Добавляем 14px свободного места внизу для гравировки (только в развернутом виде) ---
const origComputeSize = nodeType.prototype.computeSize;
nodeType.prototype.computeSize = function () {
const size = origComputeSize ? origComputeSize.apply(this, arguments) : [220, 100];
if (!this.flags?.collapsed) {
size[1] += 14;
}
return size;
};
// --- 1. ХУК НА СОЗДАНИЕ НОДЫ ---
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (origOnNodeCreated) {
origOnNodeCreated.apply(this, arguments);
}
applyBssLuxuryStyles(this);
setTimeout(() => {
applyPremiumWidgetRenderers(this);
}, 60);
};
// --- 2. ХУК НА ЗАГРУЗКУ ИЗ JSON ---
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
if (origOnConfigure) {
origOnConfigure.apply(this, arguments);
}
applyBssLuxuryStyles(this);
applyPremiumWidgetRenderers(this);
this.setSize(this.computeSize());
};
// --- 3. ОТРИСОВКА ЗАДНЕГО ФОНА ---
const origOnDrawBackground = nodeType.prototype.onDrawBackground;
nodeType.prototype.onDrawBackground = function (ctx, canvas) {
applyPremiumWidgetRenderers(this);
if (origOnDrawBackground) {
origOnDrawBackground.apply(this, arguments);
}
if (this.flags?.collapsed) return;
// Рисуем золотистый разделитель ровно над первым виджетом
if (this.widgets && this.widgets.length > 0 && this.widgets_start_y !== undefined) {
ctx.save();
const dividerY = this.widgets_start_y - 8;
ctx.strokeStyle = "rgba(212, 175, 55, 0.35)"; // Полупрозрачное благородное золото
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(12, dividerY);
ctx.lineTo(this.size[0] - 12, dividerY);
ctx.stroke();
ctx.restore();
}
};
// --- 4. ОТРИСОВКА ПЕРЕДНЕГО ПЛАНА (Гравировка на свободном месте снизу) ---
const origOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx, canvas) {
if (origOnDrawForeground) {
origOnDrawForeground.apply(this, arguments);
}
if (this.flags?.collapsed) return;
ctx.save();
// Гравировка бренда "BSS OPTIMIZED" в правом нижнем углу на свободном месте (14px отступа)
ctx.font = "italic bold 7px 'Segoe UI', system-ui, sans-serif";
ctx.fillStyle = "rgba(212, 175, 55, 0.45)"; // Золотой цвет гравировки
ctx.textAlign = "right";
ctx.letterSpacing = "0.5px";
ctx.fillText("BSS OPTIMIZED", this.size[0] - 10, this.size[1] - 8);
ctx.restore();
};
}
}
});