Files
akawana-ComfyUI-RGBYP-Mask-…/js/RGBYPMaskEditor_io.js
T
2026-02-11 01:36:51 +08:00

419 lines
13 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { uploadFile, loadImage, updateWidgetValue, getWidgetValue, getWidgetJSON, getImageWidgetFileAndFolder } from "./AKGeneralUtils.js";
import { GP } from "./RGBYPMaskEditor.js";
import { getNodeState } from "./RGBYPMaskEditor.js";
function loadImageFromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (e) => {
console.error("[RGBYP] loadImageFromUrl failed:", url, e);
reject(e);
};
img.src = url;
});
}
function waitForImage(img) {
return new Promise((resolve, reject) => {
if (!img) return reject(new Error("[RGBYP] waitForImage: no image"));
if (img.complete && img.naturalWidth > 0) return resolve(img);
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
});
}
function withCacheBust(url) {
const sep = url.includes("?") ? "&" : "?";
return `${url}${sep}_t=${Date.now()}`;
}
async function loadClipspaceImage(nameOnly) {
const img = await loadImage(nameOnly, "input", "clipspace");
try {
img.src = withCacheBust(img.src);
} catch (_) { }
return await waitForImage(img);
}
function safeBaseNameFromPath(filename) {
const lastSlash = filename.lastIndexOf("/");
const onlyName = lastSlash >= 0 ? filename.slice(lastSlash + 1) : filename;
const dot = onlyName.lastIndexOf(".");
return dot >= 0 ? onlyName.slice(0, dot) : onlyName;
}
function parseRgbypFilename(filename) {
const lastSlash = filename.lastIndexOf("/");
const nameOnly = lastSlash >= 0 ? filename.slice(lastSlash + 1) : filename;
const m = nameOnly.match(/^(.*?)-rgbyp(?:-(mask|composite|original))?-(\d+)(?:-(\d+))?\.(png|webp|jpg|jpeg)$/i);
if (!m) return null;
const kind = (m[2] || "original").toLowerCase();
return {
originalBase: m[1],
kind,
nodeId: m[3],
timestamp: m[4] || null,
ext: m[5],
};
}
function buildNamesFromOriginal(originalName, nodeId) {
const info = parseRgbypFilename(originalName);
if (!info) return null;
const base = info.originalBase;
const ext = `.${info.ext || "png"}`;
const nid = nodeId || info.nodeId;
return {
originalBase: base,
originalName: `${base}-rgbyp-original-${nid}${ext}`,
maskName: `${base}-rgbyp-mask-${nid}${ext}`,
compositeName: `${base}-rgbyp-composite-${nid}${ext}`,
};
}
function canvasToBlob(canvas, mime = "image/png", quality) {
return new Promise((resolve) => {
canvas.toBlob((b) => resolve(b), mime, quality);
});
}
async function loadBaseImg(node) {
let baseImg = null;
if (node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0) {
const memImg = node.imgs[0];
const w = memImg?.naturalWidth || memImg?.width || 0;
const h = memImg?.naturalHeight || memImg?.height || 0;
if (memImg instanceof Image && w > 0 && h > 0) {
try {
const c = document.createElement("canvas");
c.width = w;
c.height = h;
const cx = c.getContext("2d");
cx.drawImage(memImg, 0, 0);
// Use existing helper from this file (it is already defined above):
// async function canvasToBlob(canvas, mime, quality) { ... }
const blob = await canvasToBlob(c, "image/png", 1.0);
const url = URL.createObjectURL(blob);
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = () => resolve(true);
img.onerror = (e) => reject(e);
img.src = url;
});
URL.revokeObjectURL(url);
console.log("[RGBYP] initBaseImageAndCanvas: cloned node.imgs[0] into new Image", { w, h });
baseImg = img;
} catch (e) {
console.warn("[RGBYP] initBaseImageAndCanvas: clone from node.imgs[0] failed, fallback to URL", e);
}
}
}
return baseImg;
}
export function initBaseImageAndCanvas() {
const node = GP.baseNode;
const state = node ? getNodeState(node.id) : null;
if (!node || !state || !state.originalCanvas || !state.maskCanvas || !state.canvasContainer) {
console.warn("[RGBYP] initBaseImageAndCanvas: no node or canvases");
return;
}
let fallbackSrc = null;
if (node.imgs && Array.isArray(node.imgs) && node.imgs.length > 0 && node.imgs[0]?.src) {
fallbackSrc = node.imgs[0].src;
} else if (node.image instanceof Image && node.image.src) {
fallbackSrc = node.image.src;
}
if (!fallbackSrc) {
console.warn("[RGBYP] initBaseImageAndCanvas: no image src on node");
return;
}
(async () => {
const rgbypJson = getWidgetJSON(node, "rgbyp_json");
let baseImg = null;
let maskImg = null;
if (rgbypJson) {
try {
baseImg = await loadClipspaceImage(rgbypJson.original);
} catch (_) {
baseImg = null;
}
if (rgbypJson.mask) {
try {
maskImg = await loadClipspaceImage(rgbypJson.mask);
} catch (_) {
maskImg = null;
}
}
const names = buildNamesFromOriginal(rgbypJson.original, String(node.id));
if (names) {
state.rgbypNames = names;
}
state.rgbypHadJsonOnOpen = true;
} else {
state.rgbypHadJsonOnOpen = false;
}
// if (!baseImg) {
// try {
// baseImg = await loadImageFromUrl(withCacheBust(fallbackSrc));
// } catch (e) {
// console.error("[RGBYP] Failed to load image from node src", fallbackSrc, e);
// return;
// }
// }
if (!baseImg)
baseImg = await loadBaseImg(node);
state.baseImg = baseImg;
state.maskImg = maskImg || null;
const imgW = baseImg.naturalWidth || baseImg.width;
const imgH = baseImg.naturalHeight || baseImg.height;
const containerDiv = state.canvasContainer;
const prevDisplayW = containerDiv.clientWidth || containerDiv.width || imgW;
const prevDisplayH = containerDiv.clientHeight || containerDiv.height || imgH;
// state.zoomPrevWidth = prevDisplayW;
// state.zoomPrevHeight = prevDisplayH;
// state.zoom = 1;
containerDiv.style.width = imgW + "px";
containerDiv.style.height = imgH + "px";
state.originalCanvas.width = imgW;
state.originalCanvas.height = imgH;
state.maskCanvas.width = imgW;
state.maskCanvas.height = imgH;
const octx = state.originalCanvas.getContext("2d");
octx.clearRect(0, 0, imgW, imgH);
octx.drawImage(baseImg, 0, 0);
const mctx = state.maskCanvas.getContext("2d");
mctx.clearRect(0, 0, imgW, imgH);
// if (maskImg) {
// mctx.drawImage(maskImg, 0, 0);
// }
if (maskImg) {
mctx.drawImage(maskImg, 0, 0, imgW, imgH);
}
fitImageToPanel(state);
// const outerContainer = state.centralPanel || containerDiv.parentElement;
// const boxW = outerContainer?.clientWidth || prevDisplayW;
// const boxH = outerContainer?.clientHeight || prevDisplayH;
// if (boxW && boxH) {
// const scale = Math.min(boxW / imgW, boxH / imgH);
// const cssW = imgW * scale;
// const cssH = imgH * scale;
// containerDiv.style.width = cssW + "px";
// containerDiv.style.height = cssH + "px";
// state.zoomBaseWidth = cssW;
// state.zoomBaseHeight = cssH;
// state.zoom = 1;
// }
// else {
// state.zoomBaseWidth = imgW;
// state.zoomBaseHeight = imgH;
// state.zoom = 1;
// }
})().catch((e) => {
console.error("[RGBYP] initBaseImageAndCanvas async error:", e);
});
}
export async function saveMask() {
const node = GP.baseNode;
const nodeType = (node.type || node.comfyClass || (node.constructor && node.constructor.name) || "") + "";
const isBridgeNode = nodeType === "RGBYPMaskBridge";
if (!node) {
console.warn("[RGBYP] saveMask: no GP.baseNode");
return;
}
const state = getNodeState(node.id);
if (!state) {
console.warn("[RGBYP] saveMask: no state for node", node.id);
return;
}
const originalCanvas = state.originalCanvas;
const maskCanvas = state.maskCanvas;
const baseImg = state.baseImg;
if (!originalCanvas || !maskCanvas || !baseImg) {
console.warn("[RGBYP] saveMask: missing canvases or baseImg");
return;
}
const timestamp = Date.now();
const ext = ".png";
let names = state.rgbypNames || null;
if (!names) {
const tempName = `RGBYPBridge-rgbyp-original-${node.id}${ext}`;
console.log("[RGBYP] tempName:", tempName);
const graphImagePath = getImageWidgetFileAndFolder(node, "image");
const rgbypJson = getWidgetJSON(node, "rgbyp_json");
const graphImageFilename = graphImagePath ? graphImagePath.fullPath : tempName;
const rgbypInfo = graphImageFilename && graphImageFilename.includes("-rgbyp-")
? parseRgbypFilename(graphImageFilename)
: null;
const originalBase = rgbypInfo
? rgbypInfo.originalBase
: safeBaseNameFromPath(graphImageFilename || "image");
const nodeId = String(node.id);
names = {
originalBase,
originalName: `${originalBase}-rgbyp-original-${nodeId}${ext}`,
maskName: `${originalBase}-rgbyp-mask-${nodeId}${ext}`,
compositeName: `${originalBase}-rgbyp-composite-${nodeId}${ext}`,
};
state.rgbypNames = names;
}
const originalName = names.originalName;
const maskName = names.maskName;
const compositeName = names.compositeName;
const hadJsonOnOpen = !!state.rgbypHadJsonOnOpen;
if (!hadJsonOnOpen) {
const tmpCanvas = document.createElement("canvas");
tmpCanvas.width = baseImg.naturalWidth || baseImg.width;
tmpCanvas.height = baseImg.naturalHeight || baseImg.height;
const tctx = tmpCanvas.getContext("2d");
tctx.drawImage(baseImg, 0, 0);
const blob = await canvasToBlob(tmpCanvas, "image/png");
if (blob) {
await uploadFile(blob, originalName, "input", "clipspace");
}
}
{
const blob = await canvasToBlob(maskCanvas, "image/png");
if (blob) {
await uploadFile(blob, maskName, "input", "clipspace");
}
}
{
const compCanvas = document.createElement("canvas");
const w = originalCanvas.width;
const h = originalCanvas.height;
compCanvas.width = w;
compCanvas.height = h;
const cctx = compCanvas.getContext("2d");
cctx.clearRect(0, 0, w, h);
cctx.drawImage(baseImg, 0, 0, w, h);
const alpha = typeof state.maskOpacity === "number"
? Math.max(0, Math.min(1, state.maskOpacity))
: 1;
cctx.globalAlpha = alpha;
cctx.drawImage(maskCanvas, 0, 0, w, h);
cctx.globalAlpha = 1;
const blob = await canvasToBlob(compCanvas, "image/png");
if (blob) {
await uploadFile(blob, compositeName, "input", "clipspace");
}
}
const jsonObj = {
rgbyp_timestamp: timestamp,
original: originalName,
mask: maskName,
composite: compositeName,
};
node.__rgbyp_skip_clear_json_once = true;
updateWidgetValue(node, "rgbyp_json", JSON.stringify(jsonObj), true);
if (!isBridgeNode) {
const widgetValue = `clipspace/${compositeName}`;
updateWidgetValue(node, "image", widgetValue, true);
} else {
try {
const img = await loadImage(compositeName, "input", "clipspace");
node.img = img;
node.imgs = Array.isArray(node.imgs) ? (node.imgs[0] = img, node.imgs) : [img];
// node.graph?.setDirtyCanvas(true, true);
} catch (e) {
console.warn("[RGBYP] saveMask: failed to update node preview", e);
}
}
}
export function fitImageToPanel(state) {
const container = state.canvasContainer;
const panel = state.centralPanel;
const canvas = state.originalCanvas;
if (!container || !panel || !canvas) return;
const imgW = canvas.width;
const imgH = canvas.height;
if (!imgW || !imgH) return;
const boxW = panel.clientWidth;
const boxH = panel.clientHeight;
if (!boxW || !boxH) return;
const scale = Math.min(boxW / imgW, boxH / imgH);
const cssW = imgW * scale;
const cssH = imgH * scale;
container.style.width = cssW + "px";
container.style.height = cssH + "px";
state.zoomBaseWidth = cssW;
state.zoomBaseHeight = cssH;
state.zoom = 1;
panel.scrollLeft = Math.max(0, (container.scrollWidth - boxW) / 2);
panel.scrollTop = Math.max(0, (container.scrollHeight - boxH) / 2);
}