Files

480 lines
22 KiB
JavaScript

import { app } from "../../scripts/app.js";
function drawImageFit(ctx, img, areaX, areaY, areaW, areaH, zoom, panX, panY) {
if (!img || !img.complete || img.width === 0 || areaW <= 0 || areaH <= 0) return;
ctx.save();
ctx.beginPath();
ctx.rect(areaX, areaY, areaW, areaH);
ctx.clip();
const scaleX = areaW / img.width;
const scaleY = areaH / img.height;
const scale = Math.min(scaleX, scaleY);
const drawW = img.width * scale * zoom;
const drawH = img.height * scale * zoom;
const centerX = areaX + areaW / 2;
const centerY = areaY + areaH / 2;
const dx = centerX - (drawW / 2) + panX;
const dy = centerY - (drawH / 2) + panY;
ctx.drawImage(img, dx, dy, drawW, drawH);
ctx.restore();
}
function drawRoundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function getImageArea(node) {
const headerHeight = 50;
const padding = 10;
const yStart = headerHeight;
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);
const h = Math.max(10, yEnd - yStart);
return { padding, yStart, w, h };
}
function getWidgetsArea(node) {
const padding = 10;
const widgetsHeight = 122; // Обновлено
const y = node.size[1] - widgetsHeight - padding;
return { y, height: widgetsHeight };
}
function isPointInImageArea(node, localX, localY) {
const { padding, yStart, w, h } = getImageArea(node);
return localX >= padding && localX <= padding + w &&
localY >= yStart && localY <= yStart + h;
}
app.registerExtension({
name: "RS.ImageCompare",
setup() {
window.addEventListener('mouseup', () => {
if (!app.graph || !app.graph._nodes) return;
for (const node of app.graph._nodes) {
if (node.type === 'RSComparer') {
if (node.rs_dragTarget || node.rs_isDragging) {
node.rs_dragTarget = null;
node.rs_isDragging = false;
app.graph.setDirtyCanvas(true, true);
}
}
}
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "RSComparer") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.rs_img1 = null;
this.rs_img2 = null;
this.rs_sliderPos = 0.5;
this.rs_isDragging = false;
this.rs_hideImg2 = false;
this.rs_dragTarget = null;
this.rs_widgetRects = [];
this.rs_sliderHover = [];
this.setSize([450, 450]);
this.min_size = [450, 380];
const hideWidget = (name) => {
const widget = this.widgets?.find(w => w.name === name);
if (widget) {
widget.hidden = true;
widget.computeSize = () => [0, 0];
widget.disabled = true;
}
};
hideWidget('zoom');
hideWidget('pan_x');
hideWidget('pan_y');
return r;
};
const onResize = nodeType.prototype.onResize;
nodeType.prototype.onResize = function (size) {
size[0] = Math.max(450, size[0]);
size[1] = Math.max(380, size[1]);
return onResize ? onResize.apply(this, arguments) : undefined;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
const zoomWidget = this.widgets?.find(w => w.name === 'zoom');
const panXWidget = this.widgets?.find(w => w.name === 'pan_x');
const panYWidget = this.widgets?.find(w => w.name === 'pan_y');
if (zoomWidget && panXWidget && panYWidget) {
zoomWidget.value = 1.0;
panXWidget.value = 0;
panYWidget.value = 0;
}
const r = onExecuted ? onExecuted.apply(this, arguments) : undefined;
const loadImage = (imgArray, imgProp) => {
if (imgArray && imgArray.length > 0) {
const imgData = imgArray[0];
const url = `/view?filename=${encodeURIComponent(imgData.filename)}&type=${imgData.type}&subfolder=${encodeURIComponent(imgData.subfolder)}&t=${Date.now()}`;
const img = new Image();
img.onload = () => {
this[imgProp] = img;
app.graph.setDirtyCanvas(true, true);
};
img.src = url;
} else {
this[imgProp] = null;
}
};
loadImage(message.image_1, "rs_img1");
loadImage(message.image_2, "rs_img2");
return r;
};
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground ? onDrawForeground.apply(this, arguments) : undefined;
if ((this.flags && this.flags.collapsed) || (!this.rs_img1 && !this.rs_img2)) return r;
ctx.save();
const getVal = (name, def) => {
const w = this.widgets?.find(w => w.name === name);
return w !== undefined ? w.value : def;
};
const zoom = getVal('zoom', 1.0);
const panPercentX = getVal('pan_x', 0);
const panPercentY = getVal('pan_y', 0);
const { padding, yStart, w, h } = getImageArea(this);
const { y: widgetsY } = getWidgetsArea(this);
let drawW = w;
let drawH = h;
if (this.rs_img2 && this.rs_img2.complete && this.rs_img2.width > 0) {
const scaleX = w / this.rs_img2.width;
const scaleY = h / this.rs_img2.height;
const scale = Math.min(scaleX, scaleY);
drawW = this.rs_img2.width * scale * zoom;
drawH = this.rs_img2.height * scale * zoom;
}
const panPixelX = (panPercentX / 100) * (drawW / 2);
const panPixelY = (panPercentY / 100) * (drawH / 2);
if (this.rs_img2) drawImageFit(ctx, this.rs_img2, padding, yStart, w, h, zoom, panPixelX, panPixelY);
if (this.rs_img1 && !this.rs_hideImg2) {
ctx.save();
const clipW = w * this.rs_sliderPos;
ctx.beginPath();
ctx.rect(padding, yStart, clipW, h);
ctx.clip();
drawImageFit(ctx, this.rs_img1, padding, yStart, w, h, zoom, panPixelX, panPixelY);
ctx.restore();
ctx.beginPath();
ctx.moveTo(padding + clipW, yStart);
ctx.lineTo(padding + clipW, yStart + h);
ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
ctx.lineWidth = 1;
ctx.stroke();
}
const ROW_HEIGHT = 24;
const ROW_GAP = 6; // Уменьшено до 6 (как отступ до кнопки сброса)
this.rs_widgetRects = [];
const widgets = [
{ 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 widgetX = padding;
const widgetW = w;
widgets.forEach((widget, i) => {
const rowY = widgetsY + i * (ROW_HEIGHT + ROW_GAP);
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.fillStyle = "#aaa";
ctx.font = "10px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
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.arc(handleX, trackY, handleSize / 2, 0, Math.PI * 2);
ctx.fill();
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.stroke();
ctx.fillStyle = "#4CAF50";
ctx.font = "12px sans-serif";
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("🔄", resetBtnX + resetBtnSize / 2, resetBtnY + resetBtnSize / 2);
this.rs_widgetRects.push({
widget: widget.name,
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,
step: widget.step
});
});
const resetAllY = widgetsY + 2 * (ROW_HEIGHT + ROW_GAP) + ROW_HEIGHT + 6;
const resetAllHeight = 22;
const buttonWidth = w;
drawRoundRect(ctx, padding, resetAllY, buttonWidth, resetAllHeight, 4);
ctx.fillStyle = "#252525";
ctx.fill();
ctx.strokeStyle = "#444";
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = "bold 12px Arial";
ctx.fillStyle = "#C0C0C0";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("🔄️ RESET ALL PARAMETERS", padding + buttonWidth / 2, resetAllY + resetAllHeight / 2);
this.rs_resetAllRect = { x: padding, y: resetAllY, w: buttonWidth, h: resetAllHeight };
ctx.restore();
return r;
};
const onMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (e, pos, graphcanvas) {
for (let rect of this.rs_widgetRects) {
if (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) {
const widget = this.widgets?.find(w => w.name === rect.widget);
if (widget) {
const clickPos = pos[0] - rect.sliderTrackX;
const normalizedClick = Math.max(0, Math.min(1, clickPos / rect.sliderTrackWidth));
let clickValue = rect.min + normalizedClick * (rect.max - rect.min);
if (rect.widget === 'zoom') {
clickValue = Math.round(clickValue * 10) / 10;
} else {
clickValue = Math.round(clickValue);
}
widget.value = clickValue;
this.rs_dragTarget = {
widget: rect.widget,
startX: pos[0],
startValue: clickValue,
sliderX: rect.sliderTrackX,
sliderWidth: rect.sliderTrackWidth,
min: rect.min,
max: rect.max,
step: rect.step
};
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
}
if (pos[0] >= rect.resetRect.x && pos[0] <= rect.resetRect.x + rect.resetRect.w &&
pos[1] >= rect.resetRect.y && pos[1] <= rect.resetRect.y + rect.resetRect.h) {
const widget = this.widgets?.find(w => w.name === rect.widget);
if (widget) {
widget.value = rect.default;
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
}
}
if (this.rs_resetAllRect && pos[0] >= this.rs_resetAllRect.x && pos[0] <= this.rs_resetAllRect.x + this.rs_resetAllRect.w &&
pos[1] >= this.rs_resetAllRect.y && pos[1] <= this.rs_resetAllRect.y + this.rs_resetAllRect.h) {
const zoomWidget = this.widgets?.find(w => w.name === 'zoom');
const panXWidget = this.widgets?.find(w => w.name === 'pan_x');
const panYWidget = this.widgets?.find(w => w.name === 'pan_y');
if (zoomWidget && panXWidget && panYWidget) {
zoomWidget.value = 1.0;
panXWidget.value = 0;
panYWidget.value = 0;
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
}
if (isPointInImageArea(this, pos[0], pos[1])) {
this.rs_isDragging = true;
const { padding, w } = getImageArea(this);
this.rs_sliderPos = Math.max(0, Math.min(1, (pos[0] - padding) / w));
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
return onMouseDown ? onMouseDown.apply(this, arguments) : false;
};
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);
let newValue = this.rs_dragTarget.startValue + deltaValue;
newValue = Math.max(this.rs_dragTarget.min, Math.min(this.rs_dragTarget.max, newValue));
if (this.rs_dragTarget.widget === 'zoom') {
newValue = Math.round(newValue * 10) / 10;
} else {
newValue = Math.round(newValue);
}
const widget = this.widgets?.find(w => w.name === this.rs_dragTarget.widget);
if (widget) {
widget.value = newValue;
graphcanvas.setDirty(true, true);
}
e.stopPropagation();
e.preventDefault();
return true;
}
if (this.rs_isDragging) {
const { padding, w } = getImageArea(this);
this.rs_sliderPos = Math.max(0, Math.min(1, (pos[0] - padding) / w));
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
return onMouseMove ? onMouseMove.apply(this, arguments) : false;
};
const onMouseUp = nodeType.prototype.onMouseUp;
nodeType.prototype.onMouseUp = function (e, pos, graphcanvas) {
if (this.rs_dragTarget || this.rs_isDragging) {
e.stopPropagation();
e.preventDefault();
}
this.rs_dragTarget = null;
this.rs_isDragging = false;
return onMouseUp ? onMouseUp.apply(this, arguments) : false;
};
const onKeyDown = nodeType.prototype.onKeyDown;
nodeType.prototype.onKeyDown = function (e, pos, graphcanvas) {
if (e.code === 'Space') {
this.rs_hideImg2 = !this.rs_hideImg2;
graphcanvas.setDirty(true, true);
e.stopPropagation();
e.preventDefault();
return true;
}
return onKeyDown ? onKeyDown.apply(this, arguments) : false;
};
}
}
});