diff --git a/js/Canvas.js b/js/Canvas.js index ac66003..3e73d20 100644 --- a/js/Canvas.js +++ b/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 {MaskTool} from "./MaskTool.js"; import {CanvasState} from "./CanvasState.js"; @@ -7,6 +9,7 @@ import {CanvasRenderer} from "./CanvasRenderer.js"; import {CanvasIO} from "./CanvasIO.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; +import { mask_editor_showing } from "./utils/mask_utils.js"; const log = createModuleLogger('Canvas'); @@ -461,4 +464,142 @@ export class Canvas { } 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(); + } } diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index a44e662..afeed8b 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -734,4 +734,36 @@ export class CanvasLayers { }, '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'); + } } diff --git a/js/CanvasView.js b/js/CanvasView.js index 47063f8..fed9607 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -761,6 +761,13 @@ async function createCanvasWidget(node, widget, app) { ]), $el("div.painter-separator"), $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", { id: "mask-mode-btn", textContent: "Draw Mask", diff --git a/js/MaskTool.js b/js/MaskTool.js index 456689f..5320d0a 100644 --- a/js/MaskTool.js +++ b/js/MaskTool.js @@ -279,4 +279,26 @@ export class MaskTool { this.y += dy; 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}).`); + } } diff --git a/js/utils/mask_utils.js b/js/utils/mask_utils.js new file mode 100644 index 0000000..21b62a2 --- /dev/null +++ b/js/utils/mask_utils.js @@ -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() +}