Modification of interfaces in some nodes

This commit is contained in:
Raykosan
2026-08-05 13:47:04 +03:00
parent c54b28fc0c
commit ff7d5e0d5b
9 changed files with 665 additions and 116 deletions
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -94,6 +94,8 @@ class LoadImageWithText:
print(f"[WRITE] Saved in: {output_path}")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
if pil_img.mode not in ('RGB', 'RGBA'):
pil_img = pil_img.convert('RGB')
@@ -120,5 +122,5 @@ NODE_CLASS_MAPPINGS = {
"LoadImageWithText": LoadImageWithText,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageWithText": "🦊 RS Image-Text ",
"LoadImageWithText": "🦊 RS Image-Text",
}
+15 -7
View File
@@ -6,6 +6,9 @@ app.registerExtension({
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "RS Image Selector") return;
const MIN_WIDTH = 300;
const MIN_HEIGHT = 400;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated) onNodeCreated.apply(this, arguments);
@@ -16,7 +19,8 @@ app.registerExtension({
this.userResized = false;
this.heartbeatInterval = null;
this.setSize([450, 500]);
this.setSize([MIN_WIDTH, MIN_HEIGHT]);
this.min_size = [MIN_WIDTH, MIN_HEIGHT];
this.buttons = [
{ label: "➕ SELECT ALL", key: "select_all", color: "#2196F3", hover: false, row: 0, col: 0 },
@@ -58,6 +62,10 @@ app.registerExtension({
const onResize = nodeType.prototype.onResize;
nodeType.prototype.onResize = function(size) {
if (onResize) onResize.apply(this, arguments);
if (this.size[0] < MIN_WIDTH) this.size[0] = MIN_WIDTH;
if (this.size[1] < MIN_HEIGHT) this.size[1] = MIN_HEIGHT;
this.userResized = true;
};
@@ -93,9 +101,9 @@ app.registerExtension({
margin: 12,
topBarH: 10,
bottomBarH: 85,
minWidth: 400,
minWidth: MIN_WIDTH,
maxWidth: 1200,
minHeight: 300,
minHeight: MIN_HEIGHT,
maxHeight: 900
};
@@ -222,7 +230,7 @@ app.registerExtension({
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const padding = 10;
const footerHeight = 70;
const footerHeight = 65;
let widgetsTotalHeight = 0;
if (this.widgets) {
@@ -242,10 +250,10 @@ app.registerExtension({
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const padding = 10;
const footerHeight = 70;
const footerHeight = 65;
const btnH = 28;
const btnGap = 5;
const btnY = this.size[1] - 65;
const btnY = this.size[1] - 70;
let widgetsTotalHeight = 0;
if (this.widgets) {
@@ -351,7 +359,7 @@ app.registerExtension({
ctx.fillText("Render Error", 10, 10);
}
const totalBtnW = this.size[0] - 50;
const totalBtnW = this.size[0] - 30;
const btnW = (totalBtnW - btnGap) / 2;
this.btnRects = [];
+2 -2
View File
@@ -699,7 +699,7 @@ app.registerExtension({
this.clickZones = [];
const startY = this.getLoraListStartY();
const padding = 10;
const rightPanelWidth = 180;
const rightPanelWidth = 170;
const availableHeight = this.size[1] - startY - 10;
const maxVisibleStyles = Math.max(1, Math.floor(availableHeight / this.rowHeight));
@@ -756,7 +756,7 @@ app.registerExtension({
this.clickZones.push({ type: "info", index: dataIdx, x: infoX, y: y, w: infoW, h: h });
const nameX = infoX + infoW + 5;
const nameW = this.size[0] - (padding * 2) - 50 - rightPanelWidth - 20 - infoW - 5;
const nameW = this.size[0] - (padding * 2) - 1 - rightPanelWidth - 20 - infoW - 5;
ctx.fillStyle = row.enabled ? "#fff" : "#777";
ctx.font = "12px sans-serif";
let displayName = row.name;
+20 -8
View File
@@ -65,13 +65,19 @@ app.registerExtension({
widget.slotName = conn.name;
widget.draw = function(ctx, node, w, y, h) {
ctx.fillStyle = "#222";
ctx.fillRect(0, y, w, h);
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(10, y, w - 20, h, 6);
} else {
ctx.rect(10, y, w - 20, h);
}
ctx.fill();
ctx.fillStyle = "#aaa";
ctx.font = "13px Arial";
ctx.textAlign = "left";
ctx.fillText(this.name, 10, y + h / 2 + 1);
ctx.fillText(this.name, 20, y + h / 2 + 1);
const toggleW = 30, toggleH = 14;
const toggleX = w - toggleW - 10;
const toggleX = w - toggleW - 20;
const toggleY = y + (h - toggleH) / 2;
ctx.fillStyle = this.value ? "#4a4" : "#666";
ctx.beginPath();
@@ -200,8 +206,8 @@ app.registerExtension({
ctx.strokeStyle = this.value === "OFF" ? "#f44336" : "#4caf50";
ctx.lineWidth = 2;
ctx.beginPath();
if (ctx.roundRect) ctx.roundRect(1, y, w - 2, h, 6);
else ctx.rect(1, y, w - 2, h);
if (ctx.roundRect) ctx.roundRect(10, y, w - 20, h, 6);
else ctx.rect(10, y, w - 20, h);
ctx.stroke();
ctx.fillStyle = "#fff";
ctx.font = "bold 13px Arial";
@@ -270,13 +276,19 @@ app.registerExtension({
widget.slotName = inputName;
widget.draw = function(ctx, node, w, y, h) {
ctx.fillStyle = "#222";
ctx.fillRect(0, y, w, h);
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(10, y, w - 20, h, 6);
} else {
ctx.rect(10, y, w - 20, h);
}
ctx.fill();
ctx.fillStyle = "#aaa";
ctx.font = "14px Arial";
ctx.textAlign = "left";
ctx.fillText(this.name, 10, y + h / 2 + 1);
ctx.fillText(this.name, 20, y + h / 2 + 1);
const toggleW = 30, toggleH = 14;
const toggleX = w - toggleW - 10;
const toggleX = w - toggleW - 20;
const toggleY = y + (h - toggleH) / 2;
ctx.fillStyle = this.value ? "#4a4" : "#666";
ctx.beginPath();
+98 -78
View File
@@ -22,17 +22,17 @@ function drawImageFit(ctx, img, areaX, areaY, areaW, areaH, zoom, panX, panY) {
ctx.restore();
}
function drawRoundedRect(ctx, x, y, w, h, r) {
function drawRoundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.arcTo(x + w, y, x + w, y + r, r);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.arcTo(x, y + h, x, y + h - r, r);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.arcTo(x, y, x + r, y, r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
@@ -40,7 +40,7 @@ function getImageArea(node) {
const headerHeight = 50;
const padding = 10;
const yStart = headerHeight;
const widgetsHeight = 112;
const widgetsHeight = 122; // Обновлено под новые отступы (3*24 + 2*6 + 6 + 22 + 10)
const widgetsGap = 10;
const yEnd = node.size[1] - widgetsHeight - widgetsGap - padding;
const w = Math.max(10, node.size[0] - padding * 2);
@@ -50,7 +50,7 @@ function getImageArea(node) {
function getWidgetsArea(node) {
const padding = 10;
const widgetsHeight = 112;
const widgetsHeight = 122; // Обновлено
const y = node.size[1] - widgetsHeight - padding;
return { y, height: widgetsHeight };
}
@@ -89,6 +89,7 @@ app.registerExtension({
this.rs_hideImg2 = false;
this.rs_dragTarget = null;
this.rs_widgetRects = [];
this.rs_sliderHover = [];
this.setSize([450, 450]);
this.min_size = [450, 380];
@@ -202,94 +203,92 @@ app.registerExtension({
}
const ROW_HEIGHT = 24;
const ROW_GAP = 4;
const LABEL_WIDTH = 50;
const INFO_WIDTH = 35;
const RESET_WIDTH = 15;
const CORNER_RADIUS = 6;
const INNER_PADDING = 8;
const SLIDER_HEIGHT = 4;
const HANDLE_SIZE = 6;
const ROW_GAP = 6; // Уменьшено до 6 (как отступ до кнопки сброса)
this.rs_widgetRects = [];
const widgets = [
{ name: 'zoom', icon: '🔎', label: 'Zoom', value: zoom, format: (v) => v.toFixed(1), default: 1.0, min: 1.0, max: 10.0, step: 0.05 },
{ name: 'pan_x', icon: '↔️', label: 'Pan H', value: panPercentX, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 },
{ name: 'pan_y', icon: '️↕️', label: 'Pan V', value: panPercentY, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 }
{ name: 'zoom', label: 'ZOOM', value: zoom, format: (v) => v.toFixed(1), default: 1.0, min: 1.0, max: 10.0, step: 0.05 },
{ name: 'pan_x', label: 'PAN H', value: panPercentX, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 },
{ name: 'pan_y', label: 'PAN V', value: panPercentY, format: (v) => Math.round(v).toString(), default: 0, min: -100, max: 100, step: 0.5 }
];
const buttonWidth = w;
const sliderWidth = buttonWidth - LABEL_WIDTH - INFO_WIDTH - RESET_WIDTH - 6 * INNER_PADDING;
const widgetX = padding;
const widgetW = w;
widgets.forEach((widget, i) => {
const rowY = widgetsY + i * (ROW_HEIGHT + ROW_GAP);
const labelX = padding + INNER_PADDING;
const sliderX = labelX + LABEL_WIDTH + INNER_PADDING * 2;
const infoX = sliderX + sliderWidth + INNER_PADDING;
const resetX = infoX + INFO_WIDTH + INNER_PADDING;
drawRoundedRect(ctx, padding, rowY, buttonWidth, ROW_HEIGHT, CORNER_RADIUS);
ctx.fillStyle = "#252525";
ctx.fill();
ctx.strokeStyle = "#444444";
const isHover = this.rs_sliderHover?.[i] || false;
const isActive = this.rs_dragTarget?.widget === widget.name;
ctx.fillStyle = isHover ? "#2a2a2a" : "#252525";
ctx.strokeStyle = isActive ? "#4CAF50" : (isHover ? "#4CAF50" : "#444");
ctx.lineWidth = 1;
drawRoundRect(ctx, widgetX, rowY, widgetW, ROW_HEIGHT, 4);
ctx.fill();
ctx.stroke();
ctx.font = "bold 12px Arial";
ctx.fillStyle = "#C0C0C0";
ctx.fillStyle = "#aaa";
ctx.font = "10px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
const labelText = `${widget.icon} ${widget.label}`;
ctx.fillText(labelText, labelX, rowY + ROW_HEIGHT / 2);
const sliderTrackY = rowY + ROW_HEIGHT / 2;
ctx.fillText(widget.label, widgetX + 8, rowY + ROW_HEIGHT / 2);
// Gap уменьшен на 40: было 90, стало 50
const trackX = widgetX + 50;
// Ширина трека увеличена на 40: было 190, стало 150
const trackW = widgetW - 142;
const trackY = rowY + ROW_HEIGHT / 2;
const trackH = 3;
ctx.fillStyle = "#444";
ctx.fillRect(trackX, trackY - trackH / 2, trackW, trackH);
const ratio = Math.max(0, Math.min(1, (widget.value - widget.min) / (widget.max - widget.min)));
const fillW = trackW * ratio;
ctx.fillStyle = "#4CAF50";
ctx.fillRect(trackX, trackY - trackH / 2, fillW, trackH);
const handleX = trackX + fillW;
const handleSize = 12;
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.moveTo(sliderX, sliderTrackY);
ctx.lineTo(sliderX + sliderWidth, sliderTrackY);
ctx.strokeStyle = "#444444";
ctx.lineWidth = SLIDER_HEIGHT;
ctx.stroke();
const normalizedValue = (widget.value - widget.min) / (widget.max - widget.min);
const handleX = sliderX + normalizedValue * sliderWidth;
ctx.beginPath();
ctx.moveTo(handleX, sliderTrackY - HANDLE_SIZE);
ctx.lineTo(handleX + HANDLE_SIZE, sliderTrackY);
ctx.lineTo(handleX, sliderTrackY + HANDLE_SIZE);
ctx.lineTo(handleX - HANDLE_SIZE, sliderTrackY);
ctx.closePath();
ctx.fillStyle = "#000000";
ctx.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.stroke();
const infoRect = { x: infoX, y: rowY + 2, w: INFO_WIDTH, h: ROW_HEIGHT - 4 };
drawRoundedRect(ctx, infoX, rowY + 2, INFO_WIDTH, ROW_HEIGHT - 4, 4);
ctx.fillStyle = "#3B3B3B";
const valueW = 50;
const valueX = widgetX + widgetW - valueW - 30;
ctx.fillStyle = "#222";
ctx.strokeStyle = "#444";
drawRoundRect(ctx, valueX, rowY + 3, valueW, ROW_HEIGHT - 6, 3);
ctx.fill();
ctx.font = "12px monospace";
ctx.fillStyle = "#e0e0e0";
ctx.stroke();
ctx.fillStyle = "#4CAF50";
ctx.font = "10px monospace";
ctx.textAlign = "center";
ctx.fillText(widget.format(widget.value), valueX + valueW / 2, rowY + ROW_HEIGHT / 2);
const resetBtnSize = 24;
const resetBtnX = widgetX + widgetW - resetBtnSize;
const resetBtnY = rowY + (ROW_HEIGHT - resetBtnSize) / 2;
ctx.fillStyle = "#252525";
ctx.strokeStyle = "#444";
drawRoundRect(ctx, resetBtnX, resetBtnY, resetBtnSize, resetBtnSize, 4);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#888";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(widget.format(widget.value), infoX + INFO_WIDTH / 2, rowY + ROW_HEIGHT / 2);
const resetRect = { x: resetX, y: rowY, w: RESET_WIDTH, h: ROW_HEIGHT };
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText("🔃", resetX + RESET_WIDTH / 2, rowY + ROW_HEIGHT / 2);
ctx.fillText("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2);
this.rs_widgetRects.push({
widget: widget.name,
sliderRect: { x: sliderX - HANDLE_SIZE, y: rowY - HANDLE_SIZE, w: sliderWidth + HANDLE_SIZE * 2, h: ROW_HEIGHT + HANDLE_SIZE * 2 },
sliderTrackX: sliderX,
sliderTrackWidth: sliderWidth,
infoRect,
resetRect,
sliderRect: { x: trackX - 6, y: rowY, w: trackW + 12, h: ROW_HEIGHT },
sliderTrackX: trackX,
sliderTrackWidth: trackW,
resetRect: { x: resetBtnX, y: resetBtnY, w: resetBtnSize, h: resetBtnSize },
default: widget.default,
min: widget.min,
max: widget.max,
@@ -299,11 +298,12 @@ app.registerExtension({
const resetAllY = widgetsY + 2 * (ROW_HEIGHT + ROW_GAP) + ROW_HEIGHT + 6;
const resetAllHeight = 22;
const buttonWidth = w;
drawRoundedRect(ctx, padding, resetAllY, buttonWidth, resetAllHeight, CORNER_RADIUS);
drawRoundRect(ctx, padding, resetAllY, buttonWidth, resetAllHeight, 4);
ctx.fillStyle = "#252525";
ctx.fill();
ctx.strokeStyle = "#444444";
ctx.strokeStyle = "#444";
ctx.lineWidth = 1;
ctx.stroke();
@@ -400,6 +400,26 @@ app.registerExtension({
const onMouseMove = nodeType.prototype.onMouseMove;
nodeType.prototype.onMouseMove = function (e, pos, graphcanvas) {
if (this.rs_dragTarget && e && e.buttons === 0) {
this.rs_dragTarget = null;
graphcanvas.setDirty(true, true);
}
if (this.rs_widgetRects && pos && !this.rs_dragTarget) {
let hoverChanged = false;
for (let i = 0; i < this.rs_widgetRects.length; i++) {
const rect = this.rs_widgetRects[i];
const isOver = pos[0] >= rect.sliderRect.x && pos[0] <= rect.sliderRect.x + rect.sliderRect.w &&
pos[1] >= rect.sliderRect.y && pos[1] <= rect.sliderRect.y + rect.sliderRect.h;
if ((this.rs_sliderHover[i] || false) !== isOver) {
this.rs_sliderHover[i] = isOver;
hoverChanged = true;
}
}
if (hoverChanged) {
graphcanvas.setDirty(true, true);
}
}
if (this.rs_dragTarget) {
const delta = pos[0] - this.rs_dragTarget.startX;
const deltaValue = (delta / this.rs_dragTarget.sliderWidth) * (this.rs_dragTarget.max - this.rs_dragTarget.min);
+467
View File
@@ -0,0 +1,467 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "RaykoStudio.ImageText",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "LoadImageWithText") return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
node.data = {
mode: "read",
prefix: "Civitai/prompt",
text: "",
selected_image: ""
};
const MIN_WIDTH = 360;
const MIN_HEIGHT = 520;
const PREVIEW_FIXED_HEIGHT = 250;
node.MIN_WIDTH = MIN_WIDTH;
node.MIN_HEIGHT = MIN_HEIGHT;
node.setSize([MIN_WIDTH, MIN_HEIGHT]);
const w_mode = node.widgets?.find(w => w.name === "mode");
const w_prefix = node.widgets?.find(w => w.name === "filename_prefix");
const w_text = node.widgets?.find(w => w.name === "text_input");
const w_image = node.widgets?.find(w => w.name === "image");
const syncWidget = (widget, dataKey) => {
if (widget) {
widget.value = node.data[dataKey];
widget.serializeValue = () => node.data[dataKey];
}
};
if (node.widgets) {
node.widgets.forEach(w => {
if (w.name === "mode" && w.value) node.data.mode = w.value;
if (w.name === "filename_prefix" && w.value) node.data.prefix = w.value;
if (w.name === "text_input" && w.value) node.data.text = w.value;
if (w.name === "image" && w.value) node.data.selected_image = w.value;
w.hidden = true;
w.type = "hidden";
w.computeSize = () => [0, 0];
w.computedHeight = 0;
w.draw = function() {};
if (w.element && w.element.parentNode) {
w.element.parentNode.removeChild(w.element);
}
w.serializeValue = () => {
if (w.name === "mode") return node.data.mode;
if (w.name === "filename_prefix") return node.data.prefix;
if (w.name === "text_input") return node.data.text;
if (w.name === "image") return node.data.selected_image;
return w.value;
};
});
}
node.syncData = function() {
if (node.graph) {
node.graph.setDirtyCanvas(true, true);
node.graph.changeTracker?.dispatchEvent(new Event("change"));
}
};
node.image = new Image();
node.imageLoaded = false;
node.imageLoading = false;
node.dragOver = false;
node.loadImage = function(imagePath) {
if (!imagePath || node.imageLoading) return;
node.imageLoading = true;
node.data.selected_image = imagePath;
syncWidget(w_image, 'selected_image');
let filename = imagePath;
let subfolder = "";
if (imagePath.includes("/")) {
const parts = imagePath.split("/");
subfolder = parts[0];
filename = parts.slice(1).join("/");
}
let imgUrl = `/view?filename=${encodeURIComponent(filename)}&type=input`;
if (subfolder) imgUrl += `&subfolder=${encodeURIComponent(subfolder)}`;
imgUrl += `&t=${Date.now()}`;
const img = new Image();
img.onload = () => {
node.image = img;
node.imageLoaded = true;
node.imageLoading = false;
node.syncData();
};
img.onerror = () => {
node.imageLoaded = false;
node.imageLoading = false;
node.syncData();
};
img.src = imgUrl;
};
const uploadFileAndLoad = async (file) => {
if (!file || !file.type.startsWith('image/')) return false;
const formData = new FormData();
formData.append('image', file);
formData.append('subfolder', '');
formData.append('type', 'input');
try {
const response = await fetch('/upload/image', { method: 'POST', body: formData });
if (response.ok) {
const result = await response.json();
const imageName = result.name || result.filename;
const subfolder = result.subfolder || '';
const finalName = subfolder ? `${subfolder}/${imageName}` : imageName;
node.loadImage(finalName);
return true;
}
} catch (err) {
console.error("[RS Image-Text] Upload error:", err);
}
return false;
};
node.triggerFileUpload = function() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/png, image/jpeg, image/webp, image/bmp';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
await uploadFileAndLoad(file);
fileInput.remove();
};
fileInput.click();
};
const handleCanvasDrop = (e) => {
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
const rect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale);
const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale);
const nodeW = node.size[0] * ds.scale;
const nodeH = node.size[1] * ds.scale;
if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW &&
e.clientY >= nodeY && e.clientY <= nodeY + nodeH) {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
uploadFileAndLoad(file);
}
}
};
const handleCanvasDragOver = (e) => {
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
const rect = app.canvas.canvas.getBoundingClientRect();
const ds = app.canvas.ds;
const nodeX = rect.left + ((node.pos[0] + ds.offset[0]) * ds.scale);
const nodeY = rect.top + ((node.pos[1] + ds.offset[1]) * ds.scale);
const nodeW = node.size[0] * ds.scale;
const nodeH = node.size[1] * ds.scale;
if (e.clientX >= nodeX && e.clientX <= nodeX + nodeW &&
e.clientY >= nodeY && e.clientY <= nodeY + nodeH) {
e.preventDefault();
e.stopPropagation();
}
};
app.canvas.canvas.addEventListener('dragover', handleCanvasDragOver, { capture: true });
app.canvas.canvas.addEventListener('drop', handleCanvasDrop, { capture: true });
const updateUIFromData = () => {
if (prefixInput) prefixInput.value = node.data.prefix || "";
if (customTextArea) customTextArea.value = node.data.text || "";
updateModeVisuals();
};
node.onSerialize = function(o) {
syncWidget(w_mode, 'mode');
syncWidget(w_prefix, 'prefix');
syncWidget(w_text, 'text');
syncWidget(w_image, 'selected_image');
o.data = node.data;
};
node.onConfigure = function(o) {
if (o && o.data) {
node.data = { ...node.data, ...o.data };
}
node.imageLoading = false;
node.imageLoaded = false;
if (node.data.selected_image) {
node.loadImage(node.data.selected_image);
}
updateUIFromData();
syncWidget(w_mode, 'mode');
syncWidget(w_prefix, 'prefix');
syncWidget(w_text, 'text');
syncWidget(w_image, 'selected_image');
};
node.visibilityHandler = function() {
if (!document.hidden && node.graph) {
node.graph.setDirtyCanvas(true, true);
node.syncData();
}
};
document.addEventListener("visibilitychange", node.visibilityHandler);
const mainContainer = document.createElement("div");
mainContainer.style.cssText = `
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box;
pointer-events: none;
`;
const modeContainer = document.createElement("div");
modeContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;";
const createModeBtn = (label, value) => {
const btn = document.createElement("button");
btn.textContent = label;
btn.style.cssText = `flex:1; height:28px; padding:0 12px; border:2px solid #555; border-radius:4px; font-weight:normal; cursor:pointer; transition:all 0.2s; font-size:12px; pointer-events: auto;`;
btn.onclick = () => {
node.data.mode = value;
syncWidget(w_mode, 'mode');
updateModeVisuals();
node.syncData();
};
return btn;
};
const btnRead = createModeBtn("📄 READ from IMAGE", "read");
const btnWrite = createModeBtn("✏️ WRITE to IMAGE", "write");
modeContainer.appendChild(btnRead);
modeContainer.appendChild(btnWrite);
const updateModeVisuals = () => {
const isRead = node.data.mode === "read";
btnRead.style.backgroundColor = isRead ? "#4CAF50" : "#333";
btnRead.style.borderColor = isRead ? "#81C784" : "#555";
btnRead.style.color = isRead ? "#fff" : "#aaa";
btnWrite.style.backgroundColor = !isRead ? "#2196F3" : "#333";
btnWrite.style.borderColor = !isRead ? "#64B5F6" : "#555";
btnWrite.style.color = !isRead ? "#fff" : "#aaa";
};
updateModeVisuals();
const prefixContainer = document.createElement("div");
prefixContainer.style.cssText = "display:flex; align-items:center; gap:8px; width:100%; pointer-events: auto;";
const prefixLabel = document.createElement("label");
prefixLabel.textContent = "PREFIX:";
prefixLabel.style.cssText = "color:#aaa; font-size:14px; font-weight:normal; white-space:nowrap; pointer-events: auto; cursor:default;";
const prefixInput = document.createElement("input");
prefixInput.type = "text";
prefixInput.value = node.data.prefix;
prefixInput.style.cssText = "flex:1; height:28px; padding:0 12px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; font-size:14px; box-sizing:border-box; pointer-events: auto;";
prefixInput.oninput = () => {
node.data.prefix = prefixInput.value;
syncWidget(w_prefix, 'prefix');
node.syncData();
};
prefixContainer.appendChild(prefixLabel);
prefixContainer.appendChild(prefixInput);
const textContainer = document.createElement("div");
textContainer.style.cssText = "width:100%; pointer-events: auto;";
const customTextArea = document.createElement("textarea");
customTextArea.value = node.data.text;
customTextArea.placeholder = "Enter text here...";
customTextArea.style.cssText = "width:100%; min-height:50px; padding:6px; background:#222; color:#eee; border:1px solid #444; border-radius:4px; resize: none; font-family:monospace; font-size:12px; box-sizing:border-box; pointer-events: auto;";
customTextArea.oninput = () => {
node.data.text = customTextArea.value;
syncWidget(w_text, 'text');
node.syncData();
};
textContainer.appendChild(customTextArea);
const buttonsContainer = document.createElement("div");
buttonsContainer.style.cssText = "display:flex; gap:8px; width:100%; pointer-events: auto;";
const btnUpload = document.createElement("button");
btnUpload.textContent = "📂 UPLOAD IMAGE";
btnUpload.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#4CAF50; border:2px solid #4CAF50; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;";
btnUpload.onmouseover = () => { btnUpload.style.backgroundColor = "#3a3a3a"; };
btnUpload.onmouseout = () => { btnUpload.style.backgroundColor = "#2a2a2a"; };
btnUpload.onclick = () => node.triggerFileUpload();
const btnClear = document.createElement("button");
btnClear.textContent = "🗑️ CLEAR TEXT";
btnClear.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#dc3545; border:2px solid #dc3545; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events: auto;";
btnClear.onmouseover = () => { btnClear.style.backgroundColor = "#3a3a3a"; };
btnClear.onmouseout = () => { btnClear.style.backgroundColor = "#2a2a2a"; };
btnClear.onclick = () => {
node.data.text = "";
customTextArea.value = "";
syncWidget(w_text, 'text');
node.syncData();
};
buttonsContainer.appendChild(btnUpload);
buttonsContainer.appendChild(btnClear);
mainContainer.appendChild(modeContainer);
mainContainer.appendChild(prefixContainer);
mainContainer.appendChild(textContainer);
mainContainer.appendChild(buttonsContainer);
node.addDOMWidget("custom_widgets", "customtext", mainContainer);
node.customTextArea = customTextArea;
const originalOnResize = node.onResize;
node.onResize = function(size) {
if (size[0] < MIN_WIDTH) size[0] = MIN_WIDTH;
if (size[1] < MIN_HEIGHT) size[1] = MIN_HEIGHT;
const titleBarHeight = 30;
const topPadding = 18;
const gap = 8;
const bottomPadding = 20;
const previewSpacing = 35;
const fixedElements = titleBarHeight + topPadding +
28 + gap + 28 + gap +
gap + 28 +
previewSpacing +
PREVIEW_FIXED_HEIGHT +
bottomPadding;
const textareaHeight = Math.max(50, size[1] - fixedElements);
if (customTextArea) {
customTextArea.style.height = textareaHeight + 'px';
}
if (originalOnResize) originalOnResize.apply(this, arguments);
node.setDirtyCanvas(true, true);
};
node.onDrawBackground = function(ctx) {
const w = this.size[0];
const h = this.size[1];
const radius = 8;
ctx.beginPath();
ctx.moveTo(radius, 0);
ctx.lineTo(w - radius, 0);
ctx.quadraticCurveTo(w, 0, w, radius);
ctx.lineTo(w, h - radius);
ctx.quadraticCurveTo(w, h, w - radius, h);
ctx.lineTo(radius, h);
ctx.quadraticCurveTo(0, h, 0, h - radius);
ctx.lineTo(0, radius);
ctx.quadraticCurveTo(0, 0, radius, 0);
ctx.closePath();
ctx.fillStyle = "#353535";
ctx.fill();
};
node.onDrawForeground = function(ctx) {
if (this.flags.collapsed) return;
const [w, h] = this.size;
const leftRightPadding = 10;
const bottomPadding = 20;
const previewSpacing = 35;
const previewY = h - bottomPadding - PREVIEW_FIXED_HEIGHT;
const previewWidth = w - (leftRightPadding * 2);
if (this.imageLoaded && this.image) {
const imgRatio = this.image.width / this.image.height;
let drawW, drawH;
if (previewWidth / PREVIEW_FIXED_HEIGHT > imgRatio) {
drawH = PREVIEW_FIXED_HEIGHT;
drawW = drawH * imgRatio;
} else {
drawW = previewWidth;
drawH = drawW / imgRatio;
}
const drawX = leftRightPadding + (previewWidth - drawW) / 2;
const drawY = previewY + (PREVIEW_FIXED_HEIGHT - drawH) / 2;
ctx.drawImage(this.image, drawX, drawY, drawW, drawH);
ctx.strokeStyle = this.dragOver ? "#4CAF50" : "#444";
ctx.lineWidth = this.dragOver ? 2 : 1;
ctx.strokeRect(drawX, drawY, drawW, drawH);
ctx.fillStyle = "#888";
ctx.font = "15px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(`${this.image.width} × ${this.image.height}`, w / 2, previewY - 5);
} else {
ctx.fillStyle = "#1a1a1a";
ctx.fillRect(leftRightPadding, previewY, previewWidth, PREVIEW_FIXED_HEIGHT);
ctx.strokeStyle = this.dragOver ? "#4CAF50" : "#333";
ctx.lineWidth = this.dragOver ? 2 : 1;
ctx.strokeRect(leftRightPadding, previewY, previewWidth, PREVIEW_FIXED_HEIGHT);
ctx.fillStyle = "#555";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Drop Image Here", w / 2, previewY + PREVIEW_FIXED_HEIGHT / 2);
}
};
node.onMouseMove = function() {};
node.onMouseDown = function() { return false; };
const originalOnRemoved = node.onRemoved;
node.onRemoved = function() {
document.removeEventListener("visibilitychange", node.visibilityHandler);
app.canvas.canvas.removeEventListener('dragover', handleCanvasDragOver, { capture: true });
app.canvas.canvas.removeEventListener('drop', handleCanvasDrop, { capture: true });
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
};
if (node.data.selected_image) {
node.loadImage(node.data.selected_image);
}
requestAnimationFrame(() => {
node.onResize([MIN_WIDTH, MIN_HEIGHT]);
});
return result;
};
},
setup() {}
});
+7 -2
View File
@@ -368,11 +368,15 @@ app.registerExtension({
ctx.fill();
};
// --- ИЗМЕНЕННАЯ ФУНКЦИЯ drawButton с закруглением ---
this.drawButton = function(ctx, text, x, y, w, h, iconColor = "#fff") {
const radius = 5;
ctx.fillStyle = "#242427";
ctx.fillRect(x, y, w, h);
this.drawRoundedRect(ctx, x, y, w, h, radius);
ctx.fill();
ctx.strokeStyle = "#3a3a3c";
ctx.strokeRect(x, y, w, h);
this.drawRoundedRect(ctx, x, y, w, h, radius);
ctx.stroke();
ctx.font = "bold 11px sans-serif";
ctx.textAlign = "center";
@@ -388,6 +392,7 @@ app.registerExtension({
ctx.fillStyle = "#fff";
ctx.fillText(label, x + w/2 + 8, y + h/2);
};
// ----------------------------------------------
this.drawRoundedRect = function(ctx, x, y, w, h, r) {
ctx.beginPath();
+52 -18
View File
@@ -253,8 +253,10 @@ app.registerExtension({
const canvasRect = canvasEl.getBoundingClientRect();
const ds = app.canvas.ds;
const btnY = this.size[1] - 45;
const btnW = (this.size[0] - 50) / 3;
const btnX = 15 + (buttonIndex * (btnW + 5));
const padding = 15;
const gap = 10;
const btnW = (this.size[0] - 2 * padding - 2 * gap) / 3;
const btnX = padding + (buttonIndex * (btnW + gap));
const nodeScreenX = canvasRect.left + ((this.pos[0] + ds.offset[0]) * ds.scale);
const nodeScreenY = canvasRect.top + ((this.pos[1] + ds.offset[1]) * ds.scale);
@@ -282,6 +284,7 @@ app.registerExtension({
syncData();
};
// ---- ФУНКЦИЯ РАСЧЁТА ПОЛОЖЕНИЯ ИЗОБРАЖЕНИЯ (с учётом смещения вниз) ----
const calculateImageRect = () => {
if (!app.canvas) return null;
@@ -302,11 +305,13 @@ app.registerExtension({
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const padding = 10;
const leftRightPadding = 10;
const topPadding = 18; // 10 (базовый) + 8 (дополнительный сдвиг вниз)
const bottomPadding = 10;
const footerHeight = 50;
const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (padding * 2 * scale);
const availableWidth = (node.size[0] * scale) - (padding * 2 * scale);
const availableHeight = (node.size[1] * scale) - (titleBarHeight * scale) - (widgetsHeight * scale) - (footerHeight * scale) - (topPadding * scale) - (bottomPadding * scale);
const availableWidth = (node.size[0] * scale) - (leftRightPadding * 2 * scale);
if (availableWidth <= 0 || availableHeight <= 0) return null;
@@ -326,11 +331,12 @@ app.registerExtension({
contentScale = drawW / node.image.width;
}
const drawX = nodeScreenX + (padding * scale) + ((availableWidth - drawW) / 2);
const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (padding * scale) + ((availableHeight - drawH) / 2);
const drawX = nodeScreenX + (leftRightPadding * scale) + ((availableWidth - drawW) / 2);
const drawY = nodeScreenY + (titleBarHeight * scale) + (widgetsHeight * scale) + (topPadding * scale) + ((availableHeight - drawH) / 2);
return { left: drawX, top: drawY, width: drawW, height: drawH, scale: contentScale };
};
// ----------------------------------------------------------------
const syncPosition = () => {
if (!_overlayCanvas) return;
@@ -478,6 +484,7 @@ app.registerExtension({
updateLoop();
};
// ---- ОТРИСОВКА КНОПОК И ИЗОБРАЖЕНИЯ (с учётом смещения) ----
node.onDrawForeground = function(ctx) {
if (this.flags.collapsed) return;
@@ -490,16 +497,22 @@ app.registerExtension({
}
const titleBarHeight = LiteGraph.NODE_TITLE_HEIGHT || 30;
const padding = 10;
const leftRightPadding = 10;
const topPadding = 18; // 10 + 8
const bottomPadding = 10;
const footerHeight = 50;
const btnH = 28;
const btnY = h - 45;
const btnW = (w - 50) / 3;
// Параметры кнопок (расстояние +10, закругление)
const btnPadding = 15;
const btnGap = 10;
const btnW = (w - 2 * btnPadding - 2 * btnGap) / 3;
// === РАЗМЕРЫ ИЗОБРАЖЕНИЯ НАД ПРЕВЬЮ ===
if (this.imageLoaded && this.image) {
ctx.fillStyle = "#888";
ctx.font = "11px sans-serif";
ctx.font = "15px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(
@@ -510,9 +523,9 @@ app.registerExtension({
}
// ====================================
const startY = titleBarHeight + widgetsHeight + padding;
const availableHeight = h - startY - footerHeight - padding;
const availableWidth = w - (padding * 2);
const startY = titleBarHeight + widgetsHeight + topPadding;
const availableHeight = h - startY - footerHeight - bottomPadding;
const availableWidth = w - (leftRightPadding * 2);
if (this.imageLoaded && this.image) {
const imgRatio = this.image.width / this.image.height;
@@ -526,7 +539,7 @@ app.registerExtension({
drawH = drawW / imgRatio;
}
const drawX = padding + (availableWidth - drawW) / 2;
const drawX = leftRightPadding + (availableWidth - drawW) / 2;
const drawY = startY + (availableHeight - drawH) / 2;
ctx.drawImage(this.image, drawX, drawY, drawW, drawH);
@@ -534,7 +547,7 @@ app.registerExtension({
ctx.strokeRect(drawX, drawY, drawW, drawH);
} else {
ctx.fillStyle = "#222";
ctx.fillRect(padding, startY, availableWidth, availableHeight);
ctx.fillRect(leftRightPadding, startY, availableWidth, availableHeight);
ctx.fillStyle = "#555";
ctx.font = "14px Arial";
ctx.textAlign = "center";
@@ -542,17 +555,37 @@ app.registerExtension({
ctx.fillText("Select Image...", w / 2, startY + availableHeight / 2);
}
// ---- РИСУЕМ КНОПКИ (скруглённые, с увеличенным расстоянием) ----
for (let i = 0; i < this.buttons.length; i++) {
const btn = this.buttons[i];
btn.x = 15 + (i * (btnW + 5));
btn.x = btnPadding + (i * (btnW + btnGap));
btn.y = btnY;
btn.w = btnW;
btn.h = btnH;
// Фон со скруглением
const radius = 4;
const x = btn.x, y = btn.y, w = btn.w, h = btn.h;
ctx.fillStyle = btn.hover ? "#444" : "#2a2a2a";
ctx.fillRect(btn.x, btn.y, btn.w, btn.h);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
ctx.lineTo(x + w, y + h - radius);
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
ctx.lineTo(x + radius, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
// Обводка
ctx.strokeStyle = btn.color;
ctx.strokeRect(btn.x, btn.y, btn.w, btn.h);
ctx.lineWidth = 1.5;
ctx.stroke();
// Текст
ctx.fillStyle = btn.color;
ctx.font = "bold 11px Arial";
ctx.textAlign = "center";
@@ -560,6 +593,7 @@ app.registerExtension({
ctx.fillText(btn.label, btn.x + btn.w / 2, btn.y + btn.h / 2);
}
};
// ------------------------------------------------------------
node.onMouseMove = function(event, pos) {
const [x, y] = pos;