mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 06:22:14 -03:00
Add mask editor integration to canvas workflow
Introduces the ability to open the current canvas in a mask editor, upload and retrieve mask edits, and apply them to the mask layer. Adds utility functions for mask editor state detection and control, a new 'Edit Mask' button in the UI, and methods for handling mask updates and preview refresh. Also adds a setMask method to MaskTool for precise mask placement.
This commit is contained in:
141
js/Canvas.js
141
js/Canvas.js
@@ -1,3 +1,5 @@
|
|||||||
|
import { app, ComfyApp } from "../../scripts/app.js";
|
||||||
|
import { api } from "../../scripts/api.js";
|
||||||
import {removeImage} from "./db.js";
|
import {removeImage} from "./db.js";
|
||||||
import {MaskTool} from "./MaskTool.js";
|
import {MaskTool} from "./MaskTool.js";
|
||||||
import {CanvasState} from "./CanvasState.js";
|
import {CanvasState} from "./CanvasState.js";
|
||||||
@@ -7,6 +9,7 @@ import {CanvasRenderer} from "./CanvasRenderer.js";
|
|||||||
import {CanvasIO} from "./CanvasIO.js";
|
import {CanvasIO} from "./CanvasIO.js";
|
||||||
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
import {ImageReferenceManager} from "./ImageReferenceManager.js";
|
||||||
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
import {createModuleLogger} from "./utils/LoggerUtils.js";
|
||||||
|
import { mask_editor_showing } from "./utils/mask_utils.js";
|
||||||
|
|
||||||
const log = createModuleLogger('Canvas');
|
const log = createModuleLogger('Canvas');
|
||||||
|
|
||||||
@@ -461,4 +464,142 @@ export class Canvas {
|
|||||||
}
|
}
|
||||||
log.info("Canvas destroyed");
|
log.info("Canvas destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startMaskEditor() {
|
||||||
|
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
|
if (!blob) {
|
||||||
|
log.warn("Canvas is empty, cannot open mask editor.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
const filename = `layerforge-mask-edit-${+new Date()}.png`;
|
||||||
|
formData.append("image", blob, filename);
|
||||||
|
formData.append("overwrite", "true");
|
||||||
|
formData.append("type", "temp");
|
||||||
|
|
||||||
|
const response = await api.fetchApi("/upload/image", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload image: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = api.apiURL(`/view?filename=${encodeURIComponent(data.name)}&type=${data.type}&subfolder=${data.subfolder}`);
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
img.onload = res;
|
||||||
|
img.onerror = rej;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.node.imgs = [img];
|
||||||
|
|
||||||
|
ComfyApp.copyToClipspace(this.node);
|
||||||
|
ComfyApp.clipspace_return_node = this.node;
|
||||||
|
ComfyApp.open_maskeditor();
|
||||||
|
|
||||||
|
this.editorWasShowing = false;
|
||||||
|
this.waitWhileMaskEditing();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error preparing image for mask editor:", error);
|
||||||
|
alert(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitWhileMaskEditing() {
|
||||||
|
// Czekamy, aż edytor się pojawi, a potem zniknie.
|
||||||
|
if (mask_editor_showing(app)) {
|
||||||
|
this.editorWasShowing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mask_editor_showing(app) && this.editorWasShowing) {
|
||||||
|
// Edytor był widoczny i już go nie ma
|
||||||
|
this.editorWasShowing = false;
|
||||||
|
setTimeout(() => this.handleMaskEditorClose(), 100); // Dajemy chwilę na aktualizację
|
||||||
|
} else {
|
||||||
|
setTimeout(this.waitWhileMaskEditing.bind(this), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleMaskEditorClose() {
|
||||||
|
console.log("Node object after mask editor close:", this.node);
|
||||||
|
if (!this.node.imgs || !this.node.imgs.length === 0 || !this.node.imgs[0].src) {
|
||||||
|
log.warn("Mask editor was closed without a result.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultImage = new Image();
|
||||||
|
resultImage.src = this.node.imgs[0].src;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
resultImage.onload = resolve;
|
||||||
|
resultImage.onerror = reject;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to load image from mask editor.", error);
|
||||||
|
this.node.imgs = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Używamy wymiarów naszego płótna, aby zapewnić spójność
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = this.width;
|
||||||
|
tempCanvas.height = this.height;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
// Rysujemy obrazek z edytora, który zawiera maskę w kanale alfa
|
||||||
|
tempCtx.drawImage(resultImage, 0, 0, this.width, this.height);
|
||||||
|
|
||||||
|
const imageData = tempCtx.getImageData(0, 0, this.width, this.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const originalAlpha = data[i + 3];
|
||||||
|
|
||||||
|
// Ustawiamy biały kolor
|
||||||
|
data[i] = 255; // R
|
||||||
|
data[i + 1] = 255; // G
|
||||||
|
data[i + 2] = 255; // B
|
||||||
|
|
||||||
|
// Odwracamy kanał alfa
|
||||||
|
data[i + 3] = 255 - originalAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempCtx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
const maskAsImage = new Image();
|
||||||
|
maskAsImage.src = tempCanvas.toDataURL();
|
||||||
|
await new Promise(resolve => maskAsImage.onload = resolve);
|
||||||
|
|
||||||
|
// Łączymy nową maskę z istniejącą, zamiast ją nadpisywać
|
||||||
|
const maskCtx = this.maskTool.maskCtx;
|
||||||
|
const destX = -this.maskTool.x;
|
||||||
|
const destY = -this.maskTool.y;
|
||||||
|
|
||||||
|
maskCtx.globalCompositeOperation = 'screen';
|
||||||
|
maskCtx.drawImage(maskAsImage, destX, destY);
|
||||||
|
maskCtx.globalCompositeOperation = 'source-over'; // Przywracamy domyślny tryb
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
this.saveState();
|
||||||
|
|
||||||
|
// Zaktualizuj podgląd węzła nowym, spłaszczonym obrazem
|
||||||
|
const new_preview = new Image();
|
||||||
|
const blob = await this.canvasLayers.getFlattenedCanvasAsBlob();
|
||||||
|
if (blob) {
|
||||||
|
new_preview.src = URL.createObjectURL(blob);
|
||||||
|
await new Promise(r => new_preview.onload = r);
|
||||||
|
this.node.imgs = [new_preview];
|
||||||
|
} else {
|
||||||
|
this.node.imgs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -734,4 +734,36 @@ export class CanvasLayers {
|
|||||||
}, 'image/png');
|
}, 'image/png');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFlattenedCanvasAsDataURL() {
|
||||||
|
if (this.canvasLayers.layers.length === 0) return null;
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = this.canvasLayers.width;
|
||||||
|
tempCanvas.height = this.canvasLayers.height;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const sortedLayers = [...this.canvasLayers.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||||
|
sortedLayers.forEach(layer => {
|
||||||
|
if (!layer.image) return;
|
||||||
|
|
||||||
|
tempCtx.save();
|
||||||
|
tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||||
|
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||||
|
const centerX = layer.x + layer.width / 2;
|
||||||
|
const centerY = layer.y + layer.height / 2;
|
||||||
|
tempCtx.translate(centerX, centerY);
|
||||||
|
tempCtx.rotate(layer.rotation * Math.PI / 180);
|
||||||
|
tempCtx.drawImage(
|
||||||
|
layer.image,
|
||||||
|
-layer.width / 2,
|
||||||
|
-layer.height / 2,
|
||||||
|
layer.width,
|
||||||
|
layer.height
|
||||||
|
);
|
||||||
|
tempCtx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
return tempCanvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,6 +761,13 @@ async function createCanvasWidget(node, widget, app) {
|
|||||||
]),
|
]),
|
||||||
$el("div.painter-separator"),
|
$el("div.painter-separator"),
|
||||||
$el("div.painter-button-group", {id: "mask-controls"}, [
|
$el("div.painter-button-group", {id: "mask-controls"}, [
|
||||||
|
$el("button.painter-button", {
|
||||||
|
textContent: "Edit Mask",
|
||||||
|
title: "Open the current canvas view in the mask editor",
|
||||||
|
onclick: () => {
|
||||||
|
canvas.startMaskEditor();
|
||||||
|
}
|
||||||
|
}),
|
||||||
$el("button.painter-button", {
|
$el("button.painter-button", {
|
||||||
id: "mask-mode-btn",
|
id: "mask-mode-btn",
|
||||||
textContent: "Draw Mask",
|
textContent: "Draw Mask",
|
||||||
|
|||||||
@@ -279,4 +279,26 @@ export class MaskTool {
|
|||||||
this.y += dy;
|
this.y += dy;
|
||||||
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
log.info(`Mask position updated to (${this.x}, ${this.y})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMask(image) {
|
||||||
|
// `this.x` i `this.y` przechowują pozycję lewego górnego rogu płótna maski
|
||||||
|
// względem lewego górnego rogu widoku. Zatem (-this.x, -this.y) to pozycja
|
||||||
|
// lewego górnego rogu widoku na płótnie maski.
|
||||||
|
const destX = -this.x;
|
||||||
|
const destY = -this.y;
|
||||||
|
|
||||||
|
// Wyczyść tylko ten obszar na dużym płótnie maski, który odpowiada
|
||||||
|
// widocznemu obszarowi wyjściowemu.
|
||||||
|
this.maskCtx.clearRect(destX, destY, this.canvasInstance.width, this.canvasInstance.height);
|
||||||
|
|
||||||
|
// Narysuj nowy obraz maski (który ma rozmiar obszaru wyjściowego)
|
||||||
|
// dokładnie w tym wyczyszczonym miejscu.
|
||||||
|
this.maskCtx.drawImage(image, destX, destY);
|
||||||
|
|
||||||
|
if (this.onStateChange) {
|
||||||
|
this.onStateChange();
|
||||||
|
}
|
||||||
|
this.canvasInstance.render(); // Wymuś odświeżenie, aby zobaczyć zmianę
|
||||||
|
log.info(`MaskTool updated with a new mask image at correct canvas position (${destX}, ${destY}).`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
js/utils/mask_utils.js
Normal file
43
js/utils/mask_utils.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export function new_editor(app) {
|
||||||
|
if (!app) return false;
|
||||||
|
return app.ui.settings.getSettingValue('Comfy.MaskEditor.UseNewEditor')
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_mask_editor_element(app) {
|
||||||
|
return new_editor(app) ? document.getElementById('maskEditor') : document.getElementById('maskCanvas')?.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mask_editor_showing(app) {
|
||||||
|
const editor = get_mask_editor_element(app);
|
||||||
|
return editor && editor.style.display !== "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide_mask_editor() {
|
||||||
|
if (mask_editor_showing()) document.getElementById('maskEditor').style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_mask_editor_cancel_button(app) {
|
||||||
|
if (document.getElementById("maskEditor_topBarCancelButton")) return document.getElementById("maskEditor_topBarCancelButton")
|
||||||
|
return get_mask_editor_element(app)?.parentElement?.lastChild?.childNodes[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_mask_editor_save_button(app) {
|
||||||
|
if (document.getElementById("maskEditor_topBarSaveButton")) return document.getElementById("maskEditor_topBarSaveButton")
|
||||||
|
return get_mask_editor_element(app)?.parentElement?.lastChild?.childNodes[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mask_editor_listen_for_cancel(app, callback) {
|
||||||
|
const cancel_button = get_mask_editor_cancel_button(app);
|
||||||
|
if (cancel_button && !cancel_button.filter_listener_added) {
|
||||||
|
cancel_button.addEventListener('click', callback);
|
||||||
|
cancel_button.filter_listener_added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function press_maskeditor_save(app) {
|
||||||
|
get_mask_editor_save_button(app)?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function press_maskeditor_cancel(app) {
|
||||||
|
get_mask_editor_cancel_button(app)?.click()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user