import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; import { RgthreeBaseServerNode } from "./base_node.js"; import { NodeTypesString } from "./constants.js"; import { addConnectionLayoutSupport } from "./utils.js"; import { RgthreeBaseWidget, } from "./utils_widgets.js"; import { measureText } from "./utils_canvas.js"; function imageDataToUrl(data) { return api.apiURL(`/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}&subfolder=${data.subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`); } export class RgthreeImageComparer extends RgthreeBaseServerNode { constructor(title = RgthreeImageComparer.title) { super(title); this.imageIndex = 0; this.imgs = []; this.serialize_widgets = true; this.isPointerDown = false; this.isPointerOver = false; this.pointerOverPos = [0, 0]; this.canvasWidget = null; this.properties["comparer_mode"] = "Slide"; } onExecuted(output) { var _a; (_a = super.onExecuted) === null || _a === void 0 ? void 0 : _a.call(this, output); if ("images" in output) { this.canvasWidget.value = { images: (output.images || []).map((d, i) => { return { name: i === 0 ? "A" : "B", selected: true, url: imageDataToUrl(d), }; }), }; } else { output.a_images = output.a_images || []; output.b_images = output.b_images || []; const imagesToChoose = []; const multiple = output.a_images.length + output.b_images.length > 2; for (const [i, d] of output.a_images.entries()) { imagesToChoose.push({ name: output.a_images.length > 1 || multiple ? `A${i + 1}` : "A", selected: i === 0, url: imageDataToUrl(d), }); } for (const [i, d] of output.b_images.entries()) { imagesToChoose.push({ name: output.b_images.length > 1 || multiple ? `B${i + 1}` : "B", selected: i === 0, url: imageDataToUrl(d), }); } this.canvasWidget.value = { images: imagesToChoose }; } } onSerialize(o) { var _a; super.onSerialize && super.onSerialize(o); for (let [index, widget_value] of (o.widgets_values || []).entries()) { if (((_a = this.widgets[index]) === null || _a === void 0 ? void 0 : _a.name) === "rgthree_comparer") { o.widgets_values[index] = this.widgets[index].value.images.map((d) => { d = { ...d }; delete d.img; return d; }); } } } onNodeCreated() { this.canvasWidget = this.addCustomWidget(new RgthreeImageComparerWidget("rgthree_comparer", this)); this.setSize(this.computeSize()); this.setDirtyCanvas(true, true); } setIsPointerDown(down = this.isPointerDown) { const newIsDown = down && !!app.canvas.pointer_is_down; if (this.isPointerDown !== newIsDown) { this.isPointerDown = newIsDown; this.setDirtyCanvas(true, false); } this.imageIndex = this.isPointerDown ? 1 : 0; if (this.isPointerDown) { requestAnimationFrame(() => { this.setIsPointerDown(); }); } } onMouseDown(event, pos, graphCanvas) { var _a; (_a = super.onMouseDown) === null || _a === void 0 ? void 0 : _a.call(this, event, pos, graphCanvas); this.setIsPointerDown(true); } onMouseEnter(event, pos, graphCanvas) { var _a; (_a = super.onMouseEnter) === null || _a === void 0 ? void 0 : _a.call(this, event, pos, graphCanvas); this.setIsPointerDown(!!app.canvas.pointer_is_down); this.isPointerOver = true; } onMouseLeave(event, pos, graphCanvas) { var _a; (_a = super.onMouseLeave) === null || _a === void 0 ? void 0 : _a.call(this, event, pos, graphCanvas); this.setIsPointerDown(false); this.isPointerOver = false; } onMouseMove(event, pos, graphCanvas) { var _a; (_a = super.onMouseMove) === null || _a === void 0 ? void 0 : _a.call(this, event, pos, graphCanvas); this.pointerOverPos = [...pos]; this.imageIndex = this.pointerOverPos[0] > this.size[0] / 2 ? 1 : 0; } getHelp() { return `

The ${this.type.replace("(rgthree)", "")} node compares two images on top of each other.

`; } static setUp(comfyClass, nodeData) { RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, RgthreeImageComparer); } static onRegisteredForOverride(comfyClass) { addConnectionLayoutSupport(RgthreeImageComparer, app, [ ["Left", "Right"], ["Right", "Left"], ]); setTimeout(() => { RgthreeImageComparer.category = comfyClass.category; }); } } RgthreeImageComparer.title = NodeTypesString.IMAGE_COMPARER; RgthreeImageComparer.type = NodeTypesString.IMAGE_COMPARER; RgthreeImageComparer.comfyClass = NodeTypesString.IMAGE_COMPARER; RgthreeImageComparer["@comparer_mode"] = { type: "combo", values: ["Slide", "Click"], }; class RgthreeImageComparerWidget extends RgthreeBaseWidget { constructor(name, node) { super(name); this.hitAreas = {}; this.selected = []; this._value = { images: [] }; this.node = node; } set value(v) { let cleanedVal; if (Array.isArray(v)) { cleanedVal = v.map((d, i) => { if (!d || typeof d === "string") { d = { url: d, name: i == 0 ? "A" : "B", selected: true }; } return d; }); } else { cleanedVal = v.images || []; } if (cleanedVal.length > 2) { const hasAAndB = cleanedVal.some((i) => i.name.startsWith("A")) && cleanedVal.some((i) => i.name.startsWith("B")); if (!hasAAndB) { cleanedVal = [cleanedVal[0], cleanedVal[1]]; } } let selected = cleanedVal.filter((d) => d.selected); if (!selected.length && cleanedVal.length) { cleanedVal[0].selected = true; } selected = cleanedVal.filter((d) => d.selected); if (selected.length === 1 && cleanedVal.length > 1) { cleanedVal.find((d) => !d.selected).selected = true; } this._value.images = cleanedVal; selected = cleanedVal.filter((d) => d.selected); this.setSelected(selected); } get value() { return this._value; } setSelected(selected) { this._value.images.forEach((d) => (d.selected = false)); this.node.imgs.length = 0; for (const sel of selected) { if (!sel.img) { sel.img = new Image(); sel.img.src = sel.url; this.node.imgs.push(sel.img); } sel.selected = true; } this.selected = selected; } draw(ctx, node, width, y) { var _a; this.hitAreas = {}; if (this.value.images.length > 2) { ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.font = `14px Arial`; const drawData = []; const spacing = 5; let x = 0; for (const img of this.value.images) { const width = measureText(ctx, img.name); drawData.push({ img, text: img.name, x, width: measureText(ctx, img.name), }); x += width + spacing; } x = (node.size[0] - (x - spacing)) / 2; for (const d of drawData) { ctx.fillStyle = d.img.selected ? "rgba(180, 180, 180, 1)" : "rgba(180, 180, 180, 0.5)"; ctx.fillText(d.text, x, y); this.hitAreas[d.text] = { bounds: [x, y, d.width, 14], data: d.img, onDown: this.onSelectionDown, }; x += d.width + spacing; } y += 20; } if (((_a = node.properties) === null || _a === void 0 ? void 0 : _a["comparer_mode"]) === "Click") { this.drawImage(ctx, this.selected[this.node.isPointerDown ? 1 : 0], y); } else { this.drawImage(ctx, this.selected[0], y); if (node.isPointerOver) { this.drawImage(ctx, this.selected[1], y, this.node.pointerOverPos[0]); } } } onSelectionDown(event, pos, node, bounds) { const selected = [...this.selected]; if (bounds === null || bounds === void 0 ? void 0 : bounds.data.name.startsWith("A")) { selected[0] = bounds.data; } else if (bounds === null || bounds === void 0 ? void 0 : bounds.data.name.startsWith("B")) { selected[1] = bounds.data; } this.setSelected(selected); } drawImage(ctx, image, y, cropX) { var _a, _b; if (!((_a = image === null || image === void 0 ? void 0 : image.img) === null || _a === void 0 ? void 0 : _a.naturalWidth) || !((_b = image === null || image === void 0 ? void 0 : image.img) === null || _b === void 0 ? void 0 : _b.naturalHeight)) { return; } let [nodeWidth, nodeHeight] = this.node.size; const imageAspect = (image === null || image === void 0 ? void 0 : image.img.naturalWidth) / (image === null || image === void 0 ? void 0 : image.img.naturalHeight); let height = nodeHeight - y; const widgetAspect = nodeWidth / height; let targetWidth, targetHeight; let offsetX = 0; if (imageAspect > widgetAspect) { targetWidth = nodeWidth; targetHeight = nodeWidth / imageAspect; } else { targetHeight = height; targetWidth = height * imageAspect; offsetX = (nodeWidth - targetWidth) / 2; } const widthMultiplier = (image === null || image === void 0 ? void 0 : image.img.naturalWidth) / targetWidth; const sourceX = 0; const sourceY = 0; const sourceWidth = cropX != null ? (cropX - offsetX) * widthMultiplier : image === null || image === void 0 ? void 0 : image.img.naturalWidth; const sourceHeight = image === null || image === void 0 ? void 0 : image.img.naturalHeight; const destX = (nodeWidth - targetWidth) / 2; const destY = y + (height - targetHeight) / 2; const destWidth = cropX != null ? cropX - offsetX : targetWidth; const destHeight = targetHeight; ctx.save(); ctx.beginPath(); let globalCompositeOperation = ctx.globalCompositeOperation; if (cropX) { ctx.rect(destX, destY, destWidth, destHeight); ctx.clip(); } ctx.drawImage(image === null || image === void 0 ? void 0 : image.img, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight); if (cropX != null && cropX >= (nodeWidth - targetWidth) / 2 && cropX <= targetWidth + offsetX) { ctx.beginPath(); ctx.moveTo(cropX, destY); ctx.lineTo(cropX, destY + destHeight); ctx.globalCompositeOperation = "difference"; ctx.strokeStyle = "rgba(255,255,255, 1)"; ctx.stroke(); } ctx.globalCompositeOperation = globalCompositeOperation; ctx.restore(); } computeSize(width) { return [width, 20]; } serializeValue(serializedNode, widgetIndex) { const v = []; for (const data of this._value.images) { const d = { ...data }; delete d.img; v.push(d); } return { images: v }; } } app.registerExtension({ name: "rgthree.ImageComparer", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name === RgthreeImageComparer.type) { RgthreeImageComparer.setUp(nodeType, nodeData); } }, });