Change interface RS Save Image node
This commit is contained in:
+53
-13
@@ -70,6 +70,8 @@ def combine_images_pil(images, direction='horizontal'):
|
||||
return combined
|
||||
|
||||
class RSSaveImage:
|
||||
WEB_DIRECTORY = "web"
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
@@ -86,20 +88,28 @@ class RSSaveImage:
|
||||
font_dir = os.path.join(script_dir, "fonts")
|
||||
font_list = get_font_list(font_dir)
|
||||
if not font_list:
|
||||
font_list = ["none"]
|
||||
font_list = ["default"]
|
||||
|
||||
# Дефолтные данные для интерфейса
|
||||
default_data = {
|
||||
"text": "",
|
||||
"font_name": font_list[0] if font_list else "default",
|
||||
"font_size": 50,
|
||||
"footer_height": 100,
|
||||
"theme": "light",
|
||||
"filename_prefix": "ComfyUI",
|
||||
"font_list": font_list
|
||||
}
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
"optional": {
|
||||
"text": ("STRING", {"multiline": True, "default": ""}),
|
||||
"footer_height": ("INT", {"default": 100, "min": 0, "max": 1024}),
|
||||
"font_name": (font_list, {"default": font_list[0] if font_list else "none"}),
|
||||
"font_size": ("INT", {"default": 50, "min": 1, "max": 512}),
|
||||
"theme": (["dark", "light"], {"default": "light"}),
|
||||
"node_data": ("STRING", {
|
||||
"default": json.dumps(default_data),
|
||||
"hidden": True
|
||||
}),
|
||||
},
|
||||
"optional": {},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO"
|
||||
@@ -111,9 +121,36 @@ class RSSaveImage:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
|
||||
def save_images(self, images, filename_prefix="RS_SaveImage/img",
|
||||
text="", footer_height=100, font_name=None, font_size=50,
|
||||
theme="light", prompt=None, extra_pnginfo=None):
|
||||
def save_images(self, images, node_data=None, prompt=None, extra_pnginfo=None):
|
||||
# Парсинг данных из интерфейса
|
||||
default_data = {
|
||||
"text": "",
|
||||
"font_name": "default",
|
||||
"font_size": 50,
|
||||
"footer_height": 100,
|
||||
"theme": "light",
|
||||
"filename_prefix": "ComfyUI",
|
||||
"font_list": []
|
||||
}
|
||||
|
||||
try:
|
||||
if node_data:
|
||||
data = json.loads(node_data)
|
||||
# Объединяем с дефолтом на случай если каких-то полей нет
|
||||
data = {**default_data, **data}
|
||||
else:
|
||||
data = default_data
|
||||
except Exception as e:
|
||||
print(f"Error parsing node_data: {e}")
|
||||
data = default_data
|
||||
|
||||
text = data.get("text", "")
|
||||
font_name = data.get("font_name", "default")
|
||||
font_size = data.get("font_size", 50)
|
||||
footer_height = data.get("footer_height", 100)
|
||||
theme = data.get("theme", "light")
|
||||
filename_prefix = data.get("filename_prefix", "ComfyUI")
|
||||
|
||||
filename_prefix += self.prefix_append
|
||||
|
||||
if theme == "dark":
|
||||
@@ -126,10 +163,13 @@ class RSSaveImage:
|
||||
batch_size = images.shape[0]
|
||||
|
||||
font_path = None
|
||||
if font_name and font_name != "none":
|
||||
if font_name and font_name != "none" and font_name != "default":
|
||||
font_path = os.path.join(self.font_dir, font_name)
|
||||
if not os.path.isfile(font_path):
|
||||
font_path = None
|
||||
elif font_name == "default":
|
||||
# Попытка найти дефолтный шрифт, если нужно, или оставить None для load_default
|
||||
pass
|
||||
|
||||
processed_images = []
|
||||
for i in range(batch_size):
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "RaykoSaveImage",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "RSSaveImage") {
|
||||
console.log("🦊 [RS_SaveImage] JS loaded!");
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
// Отключаем стандартное превью ComfyUI
|
||||
this.allow_preview = false;
|
||||
this._outputNode = false;
|
||||
|
||||
// Дефолтные данные интерфейса
|
||||
this.data = {
|
||||
text: "",
|
||||
font_name: "default",
|
||||
font_size: 50,
|
||||
footer_height: 100,
|
||||
theme: "light",
|
||||
filename_prefix: "ComfyUI",
|
||||
font_list: ["default"]
|
||||
};
|
||||
|
||||
this.rowHeight = 28;
|
||||
this.padding = 10;
|
||||
this.labelWidth = 120;
|
||||
this.targetWidth = 350;
|
||||
this.clickZones = [];
|
||||
this.widgetYPositions = {};
|
||||
this.widgetsHeight = 260;
|
||||
this.previewHeight = 0;
|
||||
|
||||
// === ПРЕВЮ ИЗОБРАЖЕНИЙ ===
|
||||
this.imgs = [];
|
||||
this.imageIndex = 0;
|
||||
|
||||
this.hiddenWidget = this.widgets.find(w => w.name === "node_data");
|
||||
if (this.hiddenWidget) {
|
||||
this.hiddenWidget.hidden = true;
|
||||
this.hiddenWidget.serializeValue = () => {
|
||||
this.syncData();
|
||||
return this.hiddenWidget.value;
|
||||
};
|
||||
try {
|
||||
const saved = JSON.parse(this.hiddenWidget.value);
|
||||
if (saved && typeof saved === 'object') {
|
||||
this.data = { ...this.data, ...saved };
|
||||
if(saved.font_list) this.data.font_list = saved.font_list;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("🦊 [RS_SaveImage] Error loading saved data", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.widgets) {
|
||||
this.widgets.forEach(w => w.hidden = true);
|
||||
}
|
||||
|
||||
// Устанавливаем начальный размер ноды
|
||||
this.setSize([this.targetWidth, 450]);
|
||||
const self = this;
|
||||
|
||||
// Обработка полученных изображений
|
||||
const onExecuted = this.onExecuted;
|
||||
this.onExecuted = function(message) {
|
||||
const r = onExecuted ? onExecuted.apply(this, arguments) : undefined;
|
||||
|
||||
if (message?.images) {
|
||||
this.imgs = [];
|
||||
this.imageIndex = 0;
|
||||
this.previewHeight = 0;
|
||||
|
||||
for (const image of message.images) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this.calculatePreviewSize();
|
||||
this.updateNodeSize();
|
||||
if (this.graph) this.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
img.src = `/view?filename=${encodeURIComponent(image.filename)}&type=${image.type}&subfolder=${encodeURIComponent(image.subfolder || '')}`;
|
||||
this.imgs.push(img);
|
||||
}
|
||||
|
||||
if (this.imgs.length > 0) {
|
||||
this.calculatePreviewSize();
|
||||
this.updateNodeSize();
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
this.calculatePreviewSize = function() {
|
||||
if (this.imgs.length > 0 && this.imgs[this.imageIndex]) {
|
||||
const img = this.imgs[this.imageIndex];
|
||||
const aspectRatio = img.height / img.width;
|
||||
const maxWidth = this.size[0] - 40;
|
||||
let drawHeight = maxWidth * aspectRatio;
|
||||
|
||||
const maxPreviewHeight = 400;
|
||||
if (drawHeight > maxPreviewHeight) {
|
||||
drawHeight = maxPreviewHeight;
|
||||
}
|
||||
|
||||
this.previewHeight = drawHeight + 10;
|
||||
}
|
||||
};
|
||||
|
||||
this.updateNodeSize = function() {
|
||||
const totalHeight = this.widgetsHeight + this.previewHeight + 40;
|
||||
if (this.size[1] !== totalHeight) {
|
||||
this.setSize([this.size[0], totalHeight]);
|
||||
}
|
||||
};
|
||||
|
||||
// === ПОЛНОСТЬЮ ПЕРЕОПРЕДЕЛЯЕМ onDrawBackground ===
|
||||
this.onDrawBackground = function(ctx) {
|
||||
// Рисуем превью ниже виджетов
|
||||
if (this.imgs.length > 0 && this.imgs[this.imageIndex]) {
|
||||
const img = this.imgs[this.imageIndex];
|
||||
const previewY = this.widgetsHeight + 20;
|
||||
const maxWidth = this.size[0] - 40;
|
||||
|
||||
const aspectRatio = img.height / img.width;
|
||||
let drawWidth = maxWidth;
|
||||
let drawHeight = maxWidth * aspectRatio;
|
||||
|
||||
const maxPreviewHeight = 400;
|
||||
if (drawHeight > maxPreviewHeight) {
|
||||
drawHeight = maxPreviewHeight;
|
||||
drawWidth = maxPreviewHeight / aspectRatio;
|
||||
}
|
||||
|
||||
// === ЦЕНТРИРОВАНИЕ ПО ГОРИЗОНТАЛИ ===
|
||||
const previewX = (this.size[0] - drawWidth) / 2;
|
||||
|
||||
// Рамка превью
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(previewX - 5, previewY - 5, drawWidth + 10, drawHeight + 10);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(previewX - 5, previewY - 5, drawWidth + 10, drawHeight + 10);
|
||||
|
||||
// Рисуем изображение
|
||||
try {
|
||||
ctx.drawImage(img, previewX, previewY, drawWidth, drawHeight);
|
||||
} catch (e) {
|
||||
console.error("Error drawing preview:", e);
|
||||
}
|
||||
|
||||
// Индикатор количества изображений
|
||||
if (this.imgs.length > 1) {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
||||
ctx.fillRect(previewX + drawWidth - 60, previewY + drawHeight - 25, 55, 20);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(`${this.imageIndex + 1}/${this.imgs.length}`, previewX + drawWidth - 32, previewY + drawHeight - 11);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// === ОТРИСОВКА НА ПЕРЕДНЕМ ПЛАНЕ (виджеты) ===
|
||||
const onDrawForeground = this.onDrawForeground;
|
||||
this.onDrawForeground = function(ctx, visibleRect) {
|
||||
// Вызываем оригинальную отрисовку переднего плана (для заголовка, рамок и т.д.)
|
||||
if (onDrawForeground) {
|
||||
onDrawForeground.apply(this, arguments);
|
||||
}
|
||||
|
||||
this.clickZones = [];
|
||||
const startY = 80;
|
||||
const rowH = this.rowHeight;
|
||||
const pad = this.padding;
|
||||
const labelW = this.labelWidth;
|
||||
const arrowW = 25;
|
||||
const inputW = this.size[0] - pad*2 - labelW;
|
||||
let y = startY;
|
||||
|
||||
// === TEXT ===
|
||||
this.drawLabel(ctx, "TEXT", pad, y, labelW, rowH * 2);
|
||||
this.drawMultilineField(ctx, this.data.text, pad + labelW, y, inputW, rowH * 2);
|
||||
this.clickZones.push({ type: "text", x: pad + labelW, y: y, w: inputW, h: rowH * 2 });
|
||||
this.widgetYPositions["text"] = y;
|
||||
y += rowH * 2 + 10;
|
||||
|
||||
// === FONT ===
|
||||
this.drawLabel(ctx, "FONT", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.font_name, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["font_name"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "font_name", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === FONT SIZE ===
|
||||
this.drawLabel(ctx, "FONT SIZE", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.font_size, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["font_size"] = y;
|
||||
this.clickZones.push({ type: "number", field: "font_size", x: pad + labelW + arrowW, y: y, w: inputW - arrowW*2, h: rowH, min: 1, max: 512 });
|
||||
this.clickZones.push({ type: "arrow_left", field: "font_size", x: pad + labelW, y: y, w: arrowW, h: rowH, min: 1, max: 512, step: 1 });
|
||||
this.clickZones.push({ type: "arrow_right", field: "font_size", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 1, max: 512, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === FOOTER SIZE ===
|
||||
this.drawLabel(ctx, "FOOTER SIZE", pad, y, labelW, rowH);
|
||||
this.drawNumberFieldWithArrows(ctx, this.data.footer_height, pad + labelW, y, inputW, rowH, arrowW);
|
||||
this.widgetYPositions["footer_height"] = y;
|
||||
this.clickZones.push({ type: "number", field: "footer_height", x: pad + labelW + arrowW, y: y, w: inputW - arrowW*2, h: rowH, min: 0, max: 1024 });
|
||||
this.clickZones.push({ type: "arrow_left", field: "footer_height", x: pad + labelW, y: y, w: arrowW, h: rowH, min: 0, max: 1024, step: 1 });
|
||||
this.clickZones.push({ type: "arrow_right", field: "footer_height", x: pad + labelW + inputW - arrowW, y: y, w: arrowW, h: rowH, min: 0, max: 1024, step: 1 });
|
||||
y += rowH + 5;
|
||||
|
||||
// === THEME ===
|
||||
this.drawLabel(ctx, "THEME", pad, y, labelW, rowH);
|
||||
this.drawComboField(ctx, this.data.theme, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["theme"] = y;
|
||||
this.clickZones.push({ type: "combo", field: "theme", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 5;
|
||||
|
||||
// === PREFIX ===
|
||||
this.drawLabel(ctx, "PREFIX", pad, y, labelW, rowH);
|
||||
this.drawStringField(ctx, this.data.filename_prefix, pad + labelW, y, inputW, rowH);
|
||||
this.widgetYPositions["filename_prefix"] = y;
|
||||
this.clickZones.push({ type: "string", field: "filename_prefix", x: pad + labelW, y: y, w: inputW, h: rowH });
|
||||
y += rowH + 10;
|
||||
|
||||
this.widgetsHeight = y;
|
||||
};
|
||||
|
||||
this.onMouseDown = function(e, pos, canvas) {
|
||||
if (!this.clickZones.length) return false;
|
||||
|
||||
// Проверка клика по превью для переключения изображений
|
||||
if (this.imgs.length > 1 && this.imgs[this.imageIndex]) {
|
||||
const img = this.imgs[this.imageIndex];
|
||||
const previewY = this.widgetsHeight + 20;
|
||||
const maxWidth = this.size[0] - 40;
|
||||
const aspectRatio = img.height / img.width;
|
||||
let drawWidth = maxWidth;
|
||||
let drawHeight = maxWidth * aspectRatio;
|
||||
const maxPreviewHeight = 400;
|
||||
if (drawHeight > maxPreviewHeight) {
|
||||
drawHeight = maxPreviewHeight;
|
||||
drawWidth = maxPreviewHeight / aspectRatio;
|
||||
}
|
||||
|
||||
// === ЦЕНТРИРОВАНИЕ ПО ГОРИЗОНТАЛИ (для клика) ===
|
||||
const previewX = (this.size[0] - drawWidth) / 2;
|
||||
|
||||
if (pos[0] >= previewX + drawWidth - 60 && pos[0] <= previewX + drawWidth &&
|
||||
pos[1] >= previewY + drawHeight - 25 && pos[1] <= previewY + drawHeight - 5) {
|
||||
this.imageIndex = (this.imageIndex + 1) % this.imgs.length;
|
||||
this.calculatePreviewSize();
|
||||
this.updateNodeSize();
|
||||
if (this.graph) this.graph.setDirtyCanvas(true, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const zone of this.clickZones) {
|
||||
const inX = pos[0] >= zone.x && pos[0] <= zone.x + zone.w;
|
||||
const inY = pos[1] >= zone.y && pos[1] <= zone.y + zone.h;
|
||||
if (inX && inY) {
|
||||
if (zone.type === "text") {
|
||||
self.showTextInput();
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "string") {
|
||||
self.showStringInput(zone.field);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "combo") {
|
||||
self.showComboSelector(zone.field);
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "number") {
|
||||
const current = self.data[zone.field];
|
||||
const newVal = prompt(`${zone.field}:`, current);
|
||||
if (newVal !== null) {
|
||||
let parsed = parseInt(newVal);
|
||||
if (!isNaN(parsed)) {
|
||||
parsed = Math.max(zone.min, Math.min(zone.max, parsed));
|
||||
self.data[zone.field] = parsed;
|
||||
self.updateUI();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "arrow_left") {
|
||||
let current = self.data[zone.field];
|
||||
current = current - zone.step;
|
||||
self.data[zone.field] = Math.max(zone.min, current);
|
||||
self.updateUI();
|
||||
return true;
|
||||
}
|
||||
if (zone.type === "arrow_right") {
|
||||
let current = self.data[zone.field];
|
||||
current = current + zone.step;
|
||||
self.data[zone.field] = Math.min(zone.max, current);
|
||||
self.updateUI();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.drawLabel = function(ctx, text, x, y, w, h) {
|
||||
ctx.fillStyle = "#aaa";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(text, x, y + h/2 + 4);
|
||||
};
|
||||
|
||||
// === ИСПРАВЛЕННАЯ ФУНКЦИЯ — МНОГОСТРОЧНЫЙ ТЕКСТ ===
|
||||
this.drawMultilineField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
|
||||
if (!value || value.trim() === "") {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("Enter text...", x + 5, y + h/2 + 4);
|
||||
return;
|
||||
}
|
||||
|
||||
// Разбиваем текст на строки и рисуем каждую
|
||||
const lines = value.split('\n');
|
||||
const lineHeight = 14;
|
||||
const startY = y + 10;
|
||||
|
||||
// Рисуем максимум 3 строки для отображения
|
||||
const maxLines = 3;
|
||||
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
||||
let line = lines[i];
|
||||
// Обрезаем если строка слишком длинная
|
||||
if (line.length > 35) {
|
||||
line = line.substring(0, 32) + "...";
|
||||
}
|
||||
ctx.fillText(line, x + 5, startY + (i * lineHeight));
|
||||
}
|
||||
|
||||
// Если строк больше чем показываем
|
||||
if (lines.length > maxLines) {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("...", x + 5, startY + (maxLines * lineHeight));
|
||||
}
|
||||
};
|
||||
|
||||
this.drawStringField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(value || "", x + 5, y + h/2 + 4);
|
||||
};
|
||||
|
||||
this.drawComboField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(value, x + w/2, y + h/2 + 4);
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w - 12, y + h/2 - 3);
|
||||
ctx.lineTo(x + w - 6, y + h/2 - 3);
|
||||
ctx.lineTo(x + w - 9, y + h/2 + 3);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
this.drawNumberFieldWithArrows = function(ctx, value, x, y, w, h, arrowW) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.fillStyle = "#4CAF50";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 8, y + h/2);
|
||||
ctx.lineTo(x + 16, y + h/2 - 6);
|
||||
ctx.lineTo(x + 16, y + h/2 + 6);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#4CAF50";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w - 8, y + h/2);
|
||||
ctx.lineTo(x + w - 16, y + h/2 - 6);
|
||||
ctx.lineTo(x + w - 16, y + h/2 + 6);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(value, x + w/2, y + h/2 + 4);
|
||||
};
|
||||
|
||||
this.showComboSelector = function(fieldName) {
|
||||
let list = [];
|
||||
if (fieldName === "font_name") {
|
||||
list = self.data.font_list || ["default"];
|
||||
} else if (fieldName === "theme") {
|
||||
list = ["light", "dark"];
|
||||
}
|
||||
|
||||
if (!list.length) return;
|
||||
|
||||
const menu = document.createElement("div");
|
||||
menu.style.cssText = `
|
||||
position: fixed;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 10001;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
min-width: 150px;
|
||||
`;
|
||||
|
||||
list.forEach(opt => {
|
||||
const item = document.createElement("div");
|
||||
item.textContent = opt;
|
||||
item.style.cssText = `
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
`;
|
||||
item.onmouseover = () => item.style.background = "#333";
|
||||
item.onmouseout = () => item.style.background = "#1a1a1a";
|
||||
item.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
self.data[fieldName] = opt;
|
||||
self.updateUI();
|
||||
menu.remove();
|
||||
};
|
||||
menu.appendChild(item);
|
||||
});
|
||||
|
||||
const widgetY = self.widgetYPositions[fieldName] || 200;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect() ||
|
||||
document.querySelector("canvas")?.getBoundingClientRect();
|
||||
|
||||
if (canvasRect && self.pos) {
|
||||
const menuX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const menuY = canvasRect.top + self.pos[1] + widgetY + self.rowHeight;
|
||||
menu.style.left = menuX + "px";
|
||||
menu.style.top = menuY + "px";
|
||||
} else {
|
||||
menu.style.left = "250px";
|
||||
menu.style.top = "200px";
|
||||
}
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
setTimeout(() => {
|
||||
const closeHandler = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener("mousedown", closeHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", closeHandler);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.showTextInput = function() {
|
||||
const currentValue = self.data.text || '';
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10002;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
`;
|
||||
|
||||
const input = document.createElement('textarea');
|
||||
input.value = currentValue;
|
||||
input.style.cssText = `
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
resize: none;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = '✅ SAVE';
|
||||
saveBtn.style.cssText = `
|
||||
background: #4CAF50;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
`;
|
||||
saveBtn.onmouseover = () => saveBtn.style.background = "#45a049";
|
||||
saveBtn.onmouseout = () => saveBtn.style.background = "#4CAF50";
|
||||
|
||||
popup.appendChild(input);
|
||||
popup.appendChild(saveBtn);
|
||||
|
||||
const widgetY = self.widgetYPositions["text"] || 80;
|
||||
const canvasRect = document.querySelector("#graph-canvas")?.getBoundingClientRect();
|
||||
if (canvasRect && self.pos) {
|
||||
const inputX = canvasRect.left + self.pos[0] + self.padding + self.labelWidth;
|
||||
const inputY = canvasRect.top + self.pos[1] + widgetY + (self.rowHeight * 2);
|
||||
popup.style.left = inputX + 'px';
|
||||
popup.style.top = inputY + 'px';
|
||||
}
|
||||
|
||||
document.body.appendChild(popup);
|
||||
input.focus();
|
||||
|
||||
saveBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
self.data.text = input.value;
|
||||
self.updateUI();
|
||||
popup.remove();
|
||||
};
|
||||
};
|
||||
|
||||
this.showStringInput = function(fieldName) {
|
||||
const currentValue = self.data[fieldName] || '';
|
||||
const newVal = prompt(`Enter ${fieldName}:`, currentValue);
|
||||
if (newVal !== null) {
|
||||
self.data[fieldName] = newVal;
|
||||
self.updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
this.syncData = function() {
|
||||
if (this.hiddenWidget) {
|
||||
this.hiddenWidget.value = JSON.stringify(self.data, null, 2)
|
||||
.replace(/\\u([0-9a-fA-F]{4})/g, function(match, p1) {
|
||||
return String.fromCharCode(parseInt(p1, 16));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.updateUI = function() {
|
||||
self.syncData();
|
||||
if (self.graph) self.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const onSerialize = this.onSerialize;
|
||||
this.onSerialize = function(o) {
|
||||
self.syncData();
|
||||
return onSerialize ? onSerialize.apply(this, arguments) : undefined;
|
||||
};
|
||||
|
||||
const onExecute = this.onExecute;
|
||||
this.onExecute = function() {
|
||||
self.syncData();
|
||||
return onExecute ? onExecute.apply(this, arguments) : undefined;
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🦊 [RS_SaveImage] Extension initialized");
|
||||
+30
-3
@@ -53,7 +53,7 @@ app.registerExtension({
|
||||
this.data = { ...this.data, ...saved };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("🦊 [RS_TextOverlay] Error loading saved ", e);
|
||||
console.error("🦊 [RS_TextOverlay] Error loading saved data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ app.registerExtension({
|
||||
ctx.fillText(text, x, y + h/2 + 4);
|
||||
};
|
||||
|
||||
// === ИСПРАВЛЕННАЯ ФУНКЦИЯ — МНОГОСТРОЧНЫЙ ТЕКСТ ===
|
||||
this.drawMultilineField = function(ctx, value, x, y, w, h) {
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
@@ -328,8 +329,34 @@ app.registerExtension({
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
const display = value.length > 30 ? value.substring(0, 27) + "..." : value;
|
||||
ctx.fillText(display.replace('\n', '\\n'), x + 5, y + h/2 + 4);
|
||||
|
||||
if (!value || value.trim() === "") {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("Enter text...", x + 5, y + h/2 + 4);
|
||||
return;
|
||||
}
|
||||
|
||||
// Разбиваем текст на строки и рисуем каждую
|
||||
const lines = value.split('\n');
|
||||
const lineHeight = 14;
|
||||
const startY = y + 10;
|
||||
|
||||
// Рисуем максимум 3 строки для отображения
|
||||
const maxLines = 3;
|
||||
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
||||
let line = lines[i];
|
||||
// Обрезаем если строка слишком длинная
|
||||
if (line.length > 35) {
|
||||
line = line.substring(0, 32) + "...";
|
||||
}
|
||||
ctx.fillText(line, x + 5, startY + (i * lineHeight));
|
||||
}
|
||||
|
||||
// Если строк больше чем показываем
|
||||
if (lines.length > maxLines) {
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillText("...", x + 5, startY + (maxLines * lineHeight));
|
||||
}
|
||||
};
|
||||
|
||||
this.drawComboField = function(ctx, value, x, y, w, h) {
|
||||
|
||||
Reference in New Issue
Block a user