223 lines
8.0 KiB
JavaScript
223 lines
8.0 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
|
|
console.log("[miniPaint] Extension loaded");
|
|
|
|
let miniPaintWindow = null;
|
|
let modalContainer = null;
|
|
|
|
// === Loading original image from node ===
|
|
async function getImageFromNode(node) {
|
|
// 1. Try to get file name from 'image' widget
|
|
const imageWidget = node.widgets?.find(w => w.name === "image");
|
|
if (imageWidget && imageWidget.value) {
|
|
const filename = imageWidget.value;
|
|
// Form URL for fetching file (standard ComfyUI endpoint)
|
|
const url = `/view?filename=${encodeURIComponent(filename)}`;
|
|
try {
|
|
const response = await fetch(url);
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const img = new Image();
|
|
await new Promise((resolve, reject) => {
|
|
img.onload = resolve;
|
|
img.onerror = reject;
|
|
img.src = URL.createObjectURL(blob);
|
|
});
|
|
return img;
|
|
} else {
|
|
console.warn("Failed to load image from /view, falling back to DOM element.");
|
|
}
|
|
} catch (e) {
|
|
console.warn("Fetch error, falling back to DOM element.", e);
|
|
}
|
|
}
|
|
|
|
// 2. Fallback: try to extract HTMLImageElement from node.imgs or node.image
|
|
let imgElement = null;
|
|
if (node.imgs) {
|
|
const idx = node.activated_index || 0;
|
|
const item = node.imgs[idx];
|
|
if (item && item.img) imgElement = item.img;
|
|
else if (item instanceof HTMLImageElement) imgElement = item;
|
|
else imgElement = item;
|
|
} else if (node.image) {
|
|
if (node.image.img) imgElement = node.image.img;
|
|
else if (node.image instanceof HTMLImageElement) imgElement = node.image;
|
|
else imgElement = node.image;
|
|
}
|
|
|
|
if (imgElement && imgElement instanceof HTMLImageElement) {
|
|
return imgElement;
|
|
}
|
|
throw new Error("Unable to retrieve original image from node.");
|
|
}
|
|
|
|
// === Sending image to server (temporary buffer) ===
|
|
async function uploadImageToBuffer(imageElement) {
|
|
// Use naturalWidth/Height if available, otherwise take standard dimensions
|
|
const width = imageElement.naturalWidth || imageElement.width;
|
|
const height = imageElement.naturalHeight || imageElement.height;
|
|
const tempCanvas = document.createElement("canvas");
|
|
tempCanvas.width = width;
|
|
tempCanvas.height = height;
|
|
const ctx = tempCanvas.getContext("2d");
|
|
ctx.drawImage(imageElement, 0, 0, width, height);
|
|
// Get Blob (PNG binary data)
|
|
const blob = await new Promise(resolve => tempCanvas.toBlob(resolve, "image/png"));
|
|
const formData = new FormData();
|
|
formData.append("image", blob, "mypaint_edit.png");
|
|
|
|
const response = await fetch("/mypaint/upload", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
if (!response.ok) throw new Error("Upload failed");
|
|
}
|
|
|
|
// === Save handler for specific node ===
|
|
function setupSaveHandler(node) {
|
|
// Remove previous handler for this node if exists
|
|
if (node._miniPaintHandler) {
|
|
window.removeEventListener("message", node._miniPaintHandler);
|
|
}
|
|
const handler = (e) => {
|
|
// Check that the message came from our own source
|
|
if (e.origin !== window.location.origin) {
|
|
console.warn("Ignored message from untrusted origin:", e.origin);
|
|
return;
|
|
}
|
|
if (e.data && e.data.type === "MINIPAINT_SAVED") {
|
|
const imageWidget = node.widgets?.find(w => w.name === "image");
|
|
if (imageWidget) {
|
|
imageWidget.value = e.data.filename;
|
|
if (imageWidget.callback) imageWidget.callback(e.data.filename);
|
|
node.setDirtyCanvas(true, true);
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener("message", handler);
|
|
node._miniPaintHandler = handler;
|
|
}
|
|
|
|
// === Opening in separate window ===
|
|
async function openMiniPaintWindow(node) {
|
|
const img = await getImageFromNode(node);
|
|
await uploadImageToBuffer(img);
|
|
const editorUrl = "/mypaint/index.html";
|
|
if (miniPaintWindow && !miniPaintWindow.closed) {
|
|
miniPaintWindow.focus();
|
|
miniPaintWindow.postMessage({ type: "RELOAD_BUFFER" }, window.location.origin);
|
|
} else {
|
|
miniPaintWindow = window.open(editorUrl, "ComfyMiniPaintWindow");
|
|
}
|
|
setupSaveHandler(node);
|
|
}
|
|
|
|
// === Opening in a modal window (iframe) ===
|
|
async function openMiniPaintModal(node) {
|
|
const img = await getImageFromNode(node);
|
|
await uploadImageToBuffer(img);
|
|
|
|
// Close previous modal window if exists
|
|
if (modalContainer) {
|
|
document.body.removeChild(modalContainer);
|
|
modalContainer = null;
|
|
}
|
|
|
|
// Create container
|
|
modalContainer = document.createElement("div");
|
|
modalContainer.style.position = "fixed";
|
|
modalContainer.style.top = "0";
|
|
modalContainer.style.left = "0";
|
|
modalContainer.style.width = "100%";
|
|
modalContainer.style.height = "100%";
|
|
modalContainer.style.backgroundColor = "rgba(0,0,0,0.7)";
|
|
modalContainer.style.zIndex = "100000";
|
|
modalContainer.style.display = "flex";
|
|
modalContainer.style.alignItems = "center";
|
|
modalContainer.style.justifyContent = "center";
|
|
|
|
// Create iframe with editor
|
|
const iframe = document.createElement("iframe");
|
|
iframe.src = "/mypaint/index.html";
|
|
iframe.style.width = "90%";
|
|
iframe.style.height = "90%";
|
|
iframe.style.border = "none";
|
|
iframe.style.borderRadius = "8px";
|
|
iframe.style.boxShadow = "0 4px 20px rgba(0,0,0,0.3)";
|
|
|
|
// Close button
|
|
const closeBtn = document.createElement("button");
|
|
closeBtn.innerHTML = "❌";
|
|
closeBtn.style.position = "absolute";
|
|
closeBtn.style.top = "20px";
|
|
closeBtn.style.right = "30px";
|
|
closeBtn.style.backgroundColor = "#fff";
|
|
closeBtn.style.border = "none";
|
|
closeBtn.style.borderRadius = "20px";
|
|
closeBtn.style.width = "40px";
|
|
closeBtn.style.height = "40px";
|
|
closeBtn.style.fontSize = "24px";
|
|
closeBtn.style.cursor = "pointer";
|
|
closeBtn.style.zIndex = "100001";
|
|
closeBtn.style.boxShadow = "0 2px 8px rgba(0,0,0,0.2)";
|
|
closeBtn.onclick = () => {
|
|
if (modalContainer) {
|
|
document.body.removeChild(modalContainer);
|
|
modalContainer = null;
|
|
}
|
|
};
|
|
|
|
modalContainer.appendChild(iframe);
|
|
modalContainer.appendChild(closeBtn);
|
|
document.body.appendChild(modalContainer);
|
|
|
|
setupSaveHandler(node);
|
|
}
|
|
|
|
// === Extension registration ===
|
|
app.registerExtension({
|
|
name: "demonalone.MiniPaint",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
const orig = nodeType.prototype.getExtraMenuOptions;
|
|
nodeType.prototype.getExtraMenuOptions = function(canvas, options) {
|
|
orig?.apply(this, arguments);
|
|
|
|
if (this.imgs || this.image) {
|
|
// Create submenu structure for MiniPaint
|
|
const miniPaintSubmenu = [
|
|
{
|
|
content: "Edit in miniPaint (New Window)",
|
|
callback: async () => {
|
|
try {
|
|
await openMiniPaintWindow(this);
|
|
} catch (err) {
|
|
console.error("MiniPaint (window) error:", err);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
content: "Edit in miniPaint (Pop-up)",
|
|
callback: async () => {
|
|
|
|
try {
|
|
await openMiniPaintModal(this);
|
|
} catch (err) {
|
|
console.error("MiniPaint (Pop-up) error:", err);
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
options.push({
|
|
content: "🖼️miniPaint",
|
|
has_submenu: true,
|
|
submenu: {
|
|
options: miniPaintSubmenu
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
});
|