add mask painter
add mask painter
This commit is contained in:
+2
-16
@@ -21,7 +21,7 @@ export class BrushManager {
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
// On définit le style de base des boutons et le style de surlignage
|
||||
|
||||
style.innerHTML = `
|
||||
.ls-brush-toolbar button.ls-tool-button {
|
||||
border: 1px solid white;
|
||||
@@ -29,7 +29,6 @@ export class BrushManager {
|
||||
background-color: transparent;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
/* On spécifie que le survol ne s'applique QUE si le bouton n'est PAS actif */
|
||||
.ls-brush-toolbar button.ls-tool-button:not(.active):hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -94,10 +93,8 @@ export class BrushManager {
|
||||
this.node.previewCanvas.style.cursor = 'crosshair';
|
||||
}
|
||||
|
||||
// On retire la classe active de tous les boutons-outils
|
||||
this.toolbar.querySelectorAll('.ls-tool-button').forEach(b => b.classList.remove('active'));
|
||||
|
||||
// On ajoute la classe active au bon bouton
|
||||
if (this.isEyedropperActive) {
|
||||
this.toolbar.querySelector('[data-tool="eyedropper"]').classList.add('active');
|
||||
} else {
|
||||
@@ -105,13 +102,10 @@ export class BrushManager {
|
||||
}
|
||||
}
|
||||
|
||||
// NOUVELLE FONCTION pour activer la pipette
|
||||
activateEyedropper() {
|
||||
this.isEyedropperActive = true;
|
||||
//this.node.previewCanvas.style.cursor = 'copy';
|
||||
}
|
||||
|
||||
// NOUVELLE FONCTION pour prendre la couleur
|
||||
pickColor(e) {
|
||||
const ctx = this.node.previewCanvas.getContext('2d', { willReadFrequently: true });
|
||||
const pixelData = ctx.getImageData(e.offsetX, e.offsetY, 1, 1).data;
|
||||
@@ -122,8 +116,6 @@ export class BrushManager {
|
||||
|
||||
this.isEyedropperActive = false;
|
||||
this.settings.mode = 'brush';
|
||||
//this.toolbar.querySelectorAll('[data-mode]').forEach(b => b.style.border = 'none');
|
||||
//this.toolbar.querySelector('[data-mode="brush"]').style.border = '2px solid #00F';
|
||||
this._updateToolState({ mode: 'brush' });
|
||||
}
|
||||
|
||||
@@ -197,20 +189,15 @@ export class BrushManager {
|
||||
|
||||
handleMouseEvent(e) {
|
||||
this.liveDrawingOverlay.style.cursor = 'crosshair';
|
||||
// --- DÉBUT DE LA CORRECTION DU BLOCAGE DE LA TOOLBAR ---
|
||||
// On calcule où le clic a eu lieu par rapport au canvas principal qui est SOUS l'overlay
|
||||
const mainCanvasRect = this.node.previewCanvas.getBoundingClientRect();
|
||||
const zoom = app.canvas.ds.scale || 1;
|
||||
const clickX_on_main_canvas = (e.clientX - mainCanvasRect.left) / zoom;
|
||||
const clickY_on_main_canvas = (e.clientY - mainCanvasRect.top) / zoom;
|
||||
|
||||
// Si c'est un clic gauche (mousedown) ET qu'il est sur la zone de la barre d'outils...
|
||||
if (e.type === 'mousedown' && this.node.toolbar.isClickOnToolbar(clickX_on_main_canvas, clickY_on_main_canvas)) {
|
||||
// ... on passe la main au gestionnaire de la barre d'outils.
|
||||
this.node.toolbar.handleClick(e, clickX_on_main_canvas, clickY_on_main_canvas);
|
||||
return; // On arrête tout, on ne dessine pas.
|
||||
return;
|
||||
}
|
||||
// --- FIN DE LA CORRECTION DU BLOCAGE ---
|
||||
|
||||
const activeLayer = this.node.getActiveLayer();
|
||||
if (!activeLayer) return;
|
||||
@@ -326,7 +313,6 @@ export class BrushManager {
|
||||
const finalCtx = finalCanvas.getContext('2d');
|
||||
finalCtx.drawImage(originalImage, 0, 0);
|
||||
|
||||
// On utilise bien la toile interne du manager, qui est maintenant nommée correctement
|
||||
finalCtx.drawImage(this.finalDrawingCanvas, 0, 0);
|
||||
|
||||
const blob = await new Promise(resolve => finalCanvas.toBlob(resolve, 'image/png'));
|
||||
|
||||
+17
-15
@@ -98,6 +98,9 @@ app.registerExtension({
|
||||
onDrawBackground?.apply(this, arguments);
|
||||
if (this.toolbar && this.toolbar.activeTool === 'brush') {
|
||||
this.toolbar.brushManager.positionToolbar();
|
||||
}
|
||||
if (this.toolbar && this.toolbar.activeTool === 'mask_painter') {
|
||||
this.toolbar.maskPainterManager.positionToolbar();
|
||||
}
|
||||
if (this.toolbar) {
|
||||
if (this.toolbar.maskManager?.contextualToolbar?.style.display !== 'none') {
|
||||
@@ -137,6 +140,9 @@ app.registerExtension({
|
||||
}
|
||||
if (this.toolbar?.brushManager?.toolbar) {
|
||||
this.toolbar.brushManager.toolbar.remove();
|
||||
}
|
||||
if (this.toolbar?.maskPainterManager?.toolbar) {
|
||||
this.toolbar.maskPainterManager.toolbar.remove();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -350,14 +356,14 @@ nodeType.prototype.loadStateFromConfig = function(info) {
|
||||
const props = JSON.parse(info.widgets_values[jsonWidgetIndex]);
|
||||
this.base_image_properties = props.base || null;
|
||||
this.layer_properties = props.layers || {};
|
||||
//this.preview_data = props.preview_data || {};
|
||||
|
||||
if (this.toolbar) {
|
||||
this.toolbar.textElements = props.texts || [];
|
||||
} else {
|
||||
this.loadedTextData = props.texts || [];
|
||||
}
|
||||
this.stateLoaded = true;
|
||||
//app.queuePrompt();
|
||||
|
||||
setTimeout(() => {
|
||||
this.refreshPreviewsOnly();
|
||||
}, 100);
|
||||
@@ -390,7 +396,6 @@ nodeType.prototype.refreshPreviewsOnly = async function() {
|
||||
|
||||
const ui_data = await response.json();
|
||||
|
||||
// On simule un message "executed" pour déclencher le rafraîchissement des images
|
||||
if (ui_data.layer_previews) {
|
||||
this.onExecuted({ layer_previews: ui_data.layer_previews });
|
||||
}
|
||||
@@ -439,6 +444,7 @@ nodeType.prototype.refreshPreviewsOnly = async function() {
|
||||
const sortedLayerNames = Object.keys(this.layer_properties).sort((a, b) => parseInt(a.split("_")[1]) - parseInt(b.split("_")[1]));
|
||||
|
||||
for (const layerName of sortedLayerNames) {
|
||||
|
||||
const props = this.layer_properties[layerName];
|
||||
const layerImage = this.loaded_preview_images[layerName];
|
||||
const maskName = layerName.replace("layer_", "mask_");
|
||||
@@ -1005,6 +1011,9 @@ nodeType.prototype.initializeHeaderCanvases = function() {
|
||||
container.style.margin = "0px";
|
||||
|
||||
canvas.addEventListener("mousedown", (e) => {
|
||||
if (this.toolbar?.activeTool === 'mask_painter' && this.toolbar?.maskPainterManager) {
|
||||
this.toolbar.maskPainterManager.switchLayer();
|
||||
}
|
||||
const layerName = e.currentTarget.dataset.layerName;
|
||||
if (!layerName) {
|
||||
return;
|
||||
@@ -1627,26 +1636,22 @@ nodeType.prototype.handleInternalImageLoad = function() {
|
||||
const timestamp = Date.now();
|
||||
const staticFilename = isBaseImage ? "layersystem_base.png" : `layersystem_${timestamp}.png`;
|
||||
|
||||
// --- NOUVELLE LOGIQUE : CHARGEMENT LOCAL ---
|
||||
// 1. On crée une URL locale temporaire pour l'image choisie
|
||||
const localUrl = URL.createObjectURL(originalFile);
|
||||
const newImage = new Image();
|
||||
newImage.src = localUrl;
|
||||
await new Promise(resolve => newImage.onload = resolve); // On attend que l'image soit chargée
|
||||
// --- FIN DE LA NOUVELLE LOGIQUE ---
|
||||
await new Promise(resolve => newImage.onload = resolve);
|
||||
|
||||
// On lance l'upload vers le serveur en arrière-plan
|
||||
const newFile = new File([originalFile], staticFilename, { type: originalFile.type });
|
||||
const formData = new FormData();
|
||||
formData.append('image', newFile);
|
||||
formData.append('overwrite', 'true');
|
||||
formData.append('type', 'input');
|
||||
const response = await fetch('/upload/image', { method: 'POST', body: formData });
|
||||
const data = await response.json(); // On récupère les infos du fichier sauvegardé
|
||||
const data = await response.json();
|
||||
|
||||
if (isBaseImage) {
|
||||
this.base_image_properties = { filename: data.name, details: data };
|
||||
this.loaded_preview_images['base_image'] = newImage; // On met l'image chargée dans le cache
|
||||
this.loaded_preview_images['base_image'] = newImage;
|
||||
this.basePreviewImage = newImage;
|
||||
} else {
|
||||
const existingIndices = new Set(Object.keys(this.layer_properties).map(k => parseInt(k.split('_')[1])));
|
||||
@@ -1666,15 +1671,12 @@ nodeType.prototype.handleInternalImageLoad = function() {
|
||||
rotation: 0.0, brightness: 0.0, contrast: 0.0, color_r: 1.0, color_g: 1.0, color_b: 1.0, saturation: 1.0,
|
||||
invert_mask: false, color_section_collapsed: true, layer_collapsed: false,
|
||||
};
|
||||
this.loaded_preview_images[layerName] = newImage; // On met l'image chargée dans le cache
|
||||
this.loaded_preview_images[layerName] = newImage;
|
||||
}
|
||||
|
||||
this.updatePropertiesJSON();
|
||||
this.refreshUI();
|
||||
this.redrawPreviewCanvas(); // On redessine l'aperçu localement
|
||||
|
||||
// On ne lance PAS de rendu backend
|
||||
// app.queuePrompt();
|
||||
this.redrawPreviewCanvas();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
|
||||
@@ -52,7 +52,6 @@ async handleCanvasClick(e) {
|
||||
const localX = unrotatedDx + transformedWidth / 2;
|
||||
const localY = unrotatedDy + transformedHeight / 2;
|
||||
|
||||
// --- COLLEZ CE BLOC DE DIAGNOSTIC ICI ---
|
||||
console.log("--- DIAGNOSTIC BAGUETTE MAGIQUE ---");
|
||||
console.log(`clickX (e.offsetX): ${clickX}`);
|
||||
console.log(`preview.width: ${preview.width}`);
|
||||
@@ -62,7 +61,6 @@ async handleCanvasClick(e) {
|
||||
console.log(`props.scale: ${props.scale}`);
|
||||
console.log(`props.offset_x: ${props.offset_x}`);
|
||||
console.log("---------------------------------");
|
||||
// --- FIN DU BLOC DE DIAGNOSTIC ---
|
||||
|
||||
const finalX = Math.round(localX * (layerImage.naturalWidth / transformedWidth));
|
||||
const finalY = Math.round(localY * (layerImage.naturalHeight / transformedHeight));
|
||||
|
||||
@@ -327,9 +327,7 @@ createContextualToolbar() {
|
||||
|
||||
creationContainer.append(drawMaskButton);
|
||||
editionContainer.append(reeditMaskButton, deleteMaskButton);
|
||||
|
||||
buttonContainer.append(creationContainer, editionContainer);
|
||||
|
||||
buttonContainer.append(removeBgButton);
|
||||
|
||||
[drawMaskButton, reeditMaskButton, deleteMaskButton, removeBgButton].forEach(btn => {
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
export class MaskPainterManager {
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
this.isDrawing = false;
|
||||
this.lastPoint = { x: 0, y: 0 };
|
||||
this.settings = {
|
||||
size: 30,
|
||||
hardness: 80,
|
||||
opacity: 100,
|
||||
mode: 'brush'
|
||||
};
|
||||
|
||||
this.maskCanvas = document.createElement('canvas');
|
||||
this.maskCtx = this.maskCanvas.getContext('2d', { willReadFrequently: true });
|
||||
|
||||
this.liveOverlay = null;
|
||||
this.liveCtx = null;
|
||||
|
||||
this.boundHandleMouseEvent = this.handleMouseEvent.bind(this);
|
||||
this.createSettingsToolbar();
|
||||
}
|
||||
|
||||
createSettingsToolbar() {
|
||||
if (this.toolbar) this.toolbar.remove();
|
||||
|
||||
const styleId = 'ls-mask-painter-style';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.innerHTML = `
|
||||
.ls-mask-painter-toolbar .ls-tool-button { border: 1px solid white; background-color: transparent; color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-size: 18px; width: 32px; height: 32px; padding: 2px; }
|
||||
.ls-mask-painter-toolbar .ls-tool-button:not(.active):hover { background-color: rgba(255, 255, 255, 0.15); }
|
||||
.ls-mask-painter-toolbar .ls-tool-button.active { background-color: rgba(100, 180, 255, 0.4); border-color: #64b4ff; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
this.toolbar = document.createElement("div");
|
||||
this.toolbar.className = 'ls-mask-painter-toolbar';
|
||||
Object.assign(this.toolbar.style, {
|
||||
position: 'fixed', display: 'none', zIndex: '10002',
|
||||
backgroundColor: 'rgba(40, 40, 40, 0.9)', border: '1px solid #555',
|
||||
borderRadius: '8px', padding: '8px', alignItems: 'center',
|
||||
gap: '6px', color: 'white', fontFamily: 'sans-serif'
|
||||
});
|
||||
|
||||
this.toolbar.innerHTML = `
|
||||
<label>Size:</label>
|
||||
<input type="range" min="1" max="500" value="${this.settings.size}" data-setting="size" style="width: 60px;">
|
||||
<span style="min-width: 10px;" data-value="size">${this.settings.size}</span>
|
||||
<label style="margin-left: 1px;">Hardness:</label>
|
||||
<input type="range" min="0" max="100" value="${this.settings.hardness}" data-setting="hardness" style="width: 60px;">
|
||||
<span style="min-width: 10px;" data-value="hardness">${this.settings.hardness}%</span>
|
||||
<label style="margin-left: 1px;">Opacity:</label>
|
||||
<input type="range" min="1" max="100" value="${this.settings.opacity}" data-setting="opacity" style="width: 60px;">
|
||||
<span style="min-width: 10px;" data-value="opacity">${this.settings.opacity}%</span>
|
||||
<button data-mode="brush" title="Brush (Reveal)" class="ls-tool-button">🖌️</button>
|
||||
<button data-mode="eraser" title="Eraser (Hide)" class="ls-tool-button">🧼</button>
|
||||
<button data-action="apply" style="margin-left: 15px; background-color: #4CAF50; color: white; border: none; padding: 5px 10px; cursor: pointer;">Apply</button>
|
||||
<button data-action="cancel" style="margin-left: 5px; background-color: #f44336; color: white; border: none; padding: 5px 10px; cursor: pointer;">Cancel</button>
|
||||
`;
|
||||
document.body.appendChild(this.toolbar);
|
||||
|
||||
this.toolbar.querySelector('[data-mode="brush"]').classList.add('active');
|
||||
|
||||
this.toolbar.addEventListener('input', (e) => {
|
||||
const setting = e.target.dataset.setting;
|
||||
if (setting) {
|
||||
this.settings[setting] = parseInt(e.target.value, 10);
|
||||
const suffix = (setting === 'hardness' || setting === 'opacity') ? '%' : '';
|
||||
this.toolbar.querySelector(`span[data-value="${setting}"]`).textContent = e.target.value + suffix;
|
||||
}
|
||||
});
|
||||
|
||||
this.toolbar.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('button');
|
||||
if (!target) return;
|
||||
if (target.dataset.mode) {
|
||||
this.settings.mode = target.dataset.mode;
|
||||
this.toolbar.querySelectorAll('.ls-tool-button').forEach(btn => btn.classList.remove('active'));
|
||||
target.classList.add('active');
|
||||
}
|
||||
if (target.dataset.action === 'apply') this.finalizeDrawing();
|
||||
if (target.dataset.action === 'cancel') this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
async show() {
|
||||
this.node.toolbar.activeTool = 'mask_painter';
|
||||
this.activeLayer = this.node.getActiveLayer();
|
||||
if (!this.activeLayer) {
|
||||
alert("Please select a layer to paint a mask on.");
|
||||
return;
|
||||
}
|
||||
|
||||
const layerImage = this.node.loaded_preview_images[this.activeLayer.name];
|
||||
if (!layerImage || !layerImage.naturalWidth) return;
|
||||
|
||||
const props = this.node.layer_properties[this.activeLayer.name];
|
||||
props.enabled = false;
|
||||
this.node.refreshUI();
|
||||
|
||||
const preview = this.node.previewCanvas;
|
||||
this.maskCanvas.width = layerImage.naturalWidth;
|
||||
this.maskCanvas.height = layerImage.naturalHeight;
|
||||
|
||||
this.liveOverlay = document.createElement('canvas');
|
||||
Object.assign(this.liveOverlay.style, {
|
||||
position: 'absolute', top: '0', left: '0',
|
||||
zIndex: '10001', pointerEvents: 'auto', cursor: 'none'
|
||||
});
|
||||
this.liveOverlay.width = preview.width;
|
||||
this.liveOverlay.height = preview.height;
|
||||
this.liveCtx = this.liveOverlay.getContext('2d');
|
||||
preview.parentElement.appendChild(this.liveOverlay);
|
||||
|
||||
this.liveOverlay.addEventListener('mousedown', this.boundHandleMouseEvent);
|
||||
this.liveOverlay.addEventListener('mousemove', this.boundHandleMouseEvent);
|
||||
this.liveOverlay.addEventListener('mouseup', this.boundHandleMouseEvent);
|
||||
this.liveOverlay.addEventListener('mouseleave', this.boundHandleMouseEvent);
|
||||
|
||||
this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||
|
||||
if (props.internal_mask_details) {
|
||||
const details = props.internal_mask_details;
|
||||
const url = new URL("/view", window.location.origin);
|
||||
url.searchParams.set("filename", details.name);
|
||||
url.searchParams.set("type", details.type);
|
||||
url.searchParams.set("t", Date.now());
|
||||
const existingMask = new Image();
|
||||
existingMask.crossOrigin = "anonymous";
|
||||
existingMask.src = url.href;
|
||||
await new Promise(resolve => { existingMask.onload = resolve; existingMask.onerror = resolve; });
|
||||
|
||||
const tempCtx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
||||
tempCtx.canvas.width = this.maskCanvas.width;
|
||||
tempCtx.canvas.height = this.maskCanvas.height;
|
||||
tempCtx.drawImage(existingMask, 0, 0, tempCtx.canvas.width, tempCtx.canvas.height);
|
||||
const imageData = tempCtx.getImageData(0, 0, tempCtx.canvas.width, tempCtx.canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let hasAlpha = false;
|
||||
for (let i = 3; i < data.length; i += 4) {
|
||||
if (data[i] < 255) {
|
||||
hasAlpha = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAlpha) {
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i + 3] = 255 - data[i + 3];
|
||||
data[i] = 255; data[i + 1] = 255; data[i + 2] = 255;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i + 3] = 255 - data[i];
|
||||
data[i] = data[i+1] = data[i+2] = 255;
|
||||
}
|
||||
}
|
||||
this.maskCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
} else {
|
||||
this.maskCtx.fillStyle = 'white';
|
||||
this.maskCtx.fillRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);
|
||||
}
|
||||
|
||||
this.updateLivePreview();
|
||||
this.toolbar.style.display = 'flex';
|
||||
this.positionToolbar();
|
||||
}
|
||||
|
||||
drawStamp(point) {
|
||||
const ctx = this.maskCtx;
|
||||
const radius = this.settings.size / 2;
|
||||
|
||||
const gradient = ctx.createRadialGradient(point.x, point.y, 0, point.x, point.y, radius);
|
||||
const hardnessStop = Math.max(0, Math.min(1, this.settings.hardness / 100));
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.settings.opacity / 100;
|
||||
|
||||
if (this.settings.mode === 'brush') {
|
||||
gradient.addColorStop(0, 'white');
|
||||
gradient.addColorStop(hardnessStop, 'white');
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
ctx.fillStyle = gradient;
|
||||
} else {
|
||||
gradient.addColorStop(0, 'black');
|
||||
gradient.addColorStop(hardnessStop, 'black');
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.fillStyle = gradient;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
hide() {
|
||||
if (this.activeLayer) {
|
||||
const props = this.node.layer_properties[this.activeLayer.name];
|
||||
if (props) {
|
||||
props.enabled = true;
|
||||
this.node.updatePropertiesJSON();
|
||||
}
|
||||
}
|
||||
this.toolbar.style.display = 'none';
|
||||
if (this.liveOverlay) {
|
||||
this.liveOverlay.remove();
|
||||
this.liveOverlay = null;
|
||||
this.liveCtx = null;
|
||||
}
|
||||
if (this.node.toolbar.activeTool === 'mask_painter') {
|
||||
this.node.toolbar.activeTool = null;
|
||||
|
||||
}
|
||||
this.node.refreshUI();
|
||||
}
|
||||
|
||||
async switchLayer() {
|
||||
this.hide();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
this.show();
|
||||
}
|
||||
|
||||
async finalizeDrawing() {
|
||||
const applyButton = this.toolbar.querySelector('[data-action="apply"]');
|
||||
applyButton.textContent = "Applying...";
|
||||
applyButton.disabled = true;
|
||||
|
||||
try {
|
||||
const blob = await new Promise(resolve => this.maskCanvas.toBlob(resolve, 'image/png'));
|
||||
const file = new File([blob], `temp_alpha_mask_${this.activeLayer.index}.png`, { type: "image/png" });
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
formData.append("overwrite", "true");
|
||||
formData.append("type", "input");
|
||||
|
||||
let response = await fetch("/upload/image", { method: "POST", body: formData });
|
||||
const tempAlphaMaskDetails = await response.json();
|
||||
|
||||
response = await fetch("/layersystem/finalize_painter_mask", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
alpha_mask_details: tempAlphaMaskDetails,
|
||||
layer_index: this.activeLayer.index
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${await response.text()}`);
|
||||
}
|
||||
const finalMasks = await response.json();
|
||||
|
||||
const props = this.node.layer_properties[this.activeLayer.name];
|
||||
props.internal_mask_filename = finalMasks.render_mask_details.name;
|
||||
props.internal_mask_details = finalMasks.render_mask_details;
|
||||
props.internal_preview_mask_details = finalMasks.preview_mask_details;
|
||||
props.mask_last_update = Date.now();
|
||||
this.node.updatePropertiesJSON();
|
||||
|
||||
const newMaskImage = new Image();
|
||||
const previewUrl = new URL("/view", window.location.origin);
|
||||
previewUrl.searchParams.set("filename", finalMasks.preview_mask_details.name);
|
||||
previewUrl.searchParams.set("type", "input");
|
||||
previewUrl.searchParams.set("t", props.mask_last_update);
|
||||
newMaskImage.src = previewUrl.href;
|
||||
|
||||
await new Promise(resolve => newMaskImage.onload = resolve);
|
||||
this.node.loaded_preview_images[this.activeLayer.name.replace('layer_', 'mask_')] = newMaskImage;
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to apply mask drawing:", e);
|
||||
} finally {
|
||||
applyButton.textContent = "Apply Mask";
|
||||
applyButton.disabled = false;
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
getOriginalCoords(e) {
|
||||
const props = this.node.layer_properties[this.activeLayer.name];
|
||||
const layerImage = this.node.loaded_preview_images[this.activeLayer.name];
|
||||
const preview = this.node.previewCanvas;
|
||||
const toolbar = this.node.toolbar;
|
||||
const baseImage = this.node.basePreviewImage;
|
||||
const previewCanvasScale = (preview.width - toolbar.width) / baseImage.naturalWidth;
|
||||
const imageAreaCenterX = toolbar.width + (preview.width - toolbar.width) / 2;
|
||||
const imageAreaCenterY = preview.height / 2;
|
||||
const transformedWidth = layerImage.naturalWidth * props.scale * previewCanvasScale;
|
||||
const transformedHeight = layerImage.naturalHeight * props.scale * previewCanvasScale;
|
||||
const centerX = imageAreaCenterX + (props.offset_x * previewCanvasScale);
|
||||
const centerY = imageAreaCenterY + (props.offset_y * previewCanvasScale);
|
||||
const dx = e.offsetX - centerX;
|
||||
const dy = e.offsetY - centerY;
|
||||
const angleRad = -(props.rotation || 0) * Math.PI / 180;
|
||||
const unrotatedDx = dx * Math.cos(angleRad) - dy * Math.sin(angleRad);
|
||||
const unrotatedDy = dx * Math.sin(angleRad) + dy * Math.cos(angleRad);
|
||||
const localX = unrotatedDx + transformedWidth / 2;
|
||||
const localY = unrotatedDy + transformedHeight / 2;
|
||||
return {
|
||||
x: localX / (transformedWidth / layerImage.naturalWidth),
|
||||
y: localY / (transformedHeight / layerImage.naturalHeight)
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseEvent(e) {
|
||||
if (!this.activeLayer) return;
|
||||
|
||||
const mainCanvasRect = this.node.previewCanvas.getBoundingClientRect();
|
||||
const zoom = app.canvas.ds.scale || 1;
|
||||
const mouseX = (e.clientX - mainCanvasRect.left) / zoom;
|
||||
const mouseY = (e.clientY - mainCanvasRect.top) / zoom;
|
||||
const isOverToolbar = this.node.toolbar.isClickOnToolbar(mouseX, mouseY);
|
||||
|
||||
if (isOverToolbar) {
|
||||
this.liveOverlay.style.cursor = 'default';
|
||||
this.updateLivePreview(null);
|
||||
if (e.type === 'mousedown') {
|
||||
this.node.toolbar.handleClick(e, mouseX, mouseY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.liveOverlay.style.cursor = 'none';
|
||||
|
||||
if (e.type === 'mousedown') {
|
||||
this.isDrawing = true;
|
||||
const coords = this.getOriginalCoords(e);
|
||||
this.lastPoint = coords;
|
||||
this.drawStamp(coords);
|
||||
}
|
||||
else if (e.type === 'mousemove') {
|
||||
if (this.isDrawing) {
|
||||
const coords = this.getOriginalCoords(e);
|
||||
const dist = Math.hypot(coords.x - this.lastPoint.x, coords.y - this.lastPoint.y);
|
||||
const spacing = this.settings.size / 2;
|
||||
|
||||
if (dist >= spacing) {
|
||||
this.drawStroke(this.lastPoint, coords);
|
||||
this.lastPoint = coords;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.type === 'mouseup' || e.type === 'mouseleave') {
|
||||
this.isDrawing = false;
|
||||
}
|
||||
|
||||
this.updateLivePreview(e);
|
||||
}
|
||||
|
||||
drawStroke(from, to) {
|
||||
const dist = Math.hypot(to.x - from.x, to.y - from.y);
|
||||
const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
||||
const step = this.settings.size / 4;
|
||||
for (let i = 0; i < dist; i += step) {
|
||||
const x = from.x + (Math.cos(angle) * i);
|
||||
const y = from.y + (Math.sin(angle) * i);
|
||||
this.drawStamp({ x, y });
|
||||
}
|
||||
this.drawStamp(to);
|
||||
}
|
||||
|
||||
updateLivePreview(mouseEvent = null) {
|
||||
if (!this.liveCtx) return;
|
||||
this.liveCtx.clearRect(0, 0, this.liveOverlay.width, this.liveOverlay.height);
|
||||
|
||||
const tempLayerCanvas = document.createElement('canvas');
|
||||
tempLayerCanvas.width = this.maskCanvas.width;
|
||||
tempLayerCanvas.height = this.maskCanvas.height;
|
||||
const tempLayerCtx = tempLayerCanvas.getContext('2d');
|
||||
|
||||
const layerImage = this.node.loaded_preview_images[this.activeLayer.name];
|
||||
tempLayerCtx.drawImage(layerImage, 0, 0);
|
||||
tempLayerCtx.globalCompositeOperation = 'destination-in';
|
||||
tempLayerCtx.drawImage(this.maskCanvas, 0, 0);
|
||||
|
||||
const props = this.node.layer_properties[this.activeLayer.name];
|
||||
const preview = this.node.previewCanvas;
|
||||
const toolbar = this.node.toolbar;
|
||||
const baseImage = this.node.basePreviewImage;
|
||||
const previewCanvasScale = (preview.width - toolbar.width) / baseImage.naturalWidth;
|
||||
const imageAreaCenterX = toolbar.width + (preview.width - toolbar.width) / 2;
|
||||
const imageAreaCenterY = preview.height / 2;
|
||||
const transformedWidth = layerImage.naturalWidth * props.scale * previewCanvasScale;
|
||||
const transformedHeight = layerImage.naturalHeight * props.scale * previewCanvasScale;
|
||||
const centerX = imageAreaCenterX + (props.offset_x * previewCanvasScale);
|
||||
const centerY = imageAreaCenterY + (props.offset_y * previewCanvasScale);
|
||||
const angleRad = (props.rotation || 0) * Math.PI / 180;
|
||||
|
||||
this.liveCtx.save();
|
||||
this.liveCtx.translate(centerX, centerY);
|
||||
this.liveCtx.rotate(angleRad);
|
||||
this.liveCtx.drawImage(tempLayerCanvas, -transformedWidth / 2, -transformedHeight / 2, transformedWidth, transformedHeight);
|
||||
this.liveCtx.restore();
|
||||
|
||||
if (mouseEvent) {
|
||||
const previewSize = (this.settings.size / this.maskCanvas.width) * transformedWidth;
|
||||
this.liveCtx.beginPath();
|
||||
this.liveCtx.arc(mouseEvent.offsetX, mouseEvent.offsetY, previewSize / 2, 0, 2 * Math.PI);
|
||||
this.liveCtx.strokeStyle = 'white';
|
||||
this.liveCtx.lineWidth = 1;
|
||||
this.liveCtx.setLineDash([2, 2]);
|
||||
this.liveCtx.stroke();
|
||||
this.liveCtx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
positionToolbar() {
|
||||
if (!this.node.previewCanvas) return;
|
||||
const canvasRect = this.node.previewCanvas.getBoundingClientRect();
|
||||
this.toolbar.style.left = `${canvasRect.left}px`;
|
||||
this.toolbar.style.top = `${canvasRect.top - this.toolbar.offsetHeight - 5}px`;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ async performRemoveBg() {
|
||||
const layerIndex = activeLayer.index;
|
||||
const layerName = activeLayer.name;
|
||||
const previewInfo = this.node.preview_data?.[layerName];
|
||||
//const sourceFilename = previewInfo?.filename;
|
||||
const layerProps = this.node.layer_properties?.[layerName];
|
||||
const sourceFilename = layerProps?.source_filename;
|
||||
|
||||
|
||||
+12
-9
@@ -2,6 +2,7 @@ import { MaskManager } from './mask_manager.js';
|
||||
import { BrushManager } from './brush_manager.js';
|
||||
import { RemoveBgManager } from './removebg.js';
|
||||
import { MagicWandManager } from './magic_wand_manager.js';
|
||||
import { MaskPainterManager } from './mask_painter_manager.js';
|
||||
|
||||
const textIconPath = new Path2D("M5 4v2h5v12h4V6h5V4H5z");
|
||||
const maskIconPath = new Path2D("M2 2 H22 V22 H2 Z M12 12 m-6 0 a6 6 0 1 0 12 0 a6 6 0 1 0 -12 0");
|
||||
@@ -71,7 +72,7 @@ export class Toolbar {
|
||||
this.tools = [
|
||||
{ name: 'text', icon: textIconPath, y: 9 },
|
||||
{ name: 'mask', icon: '🎭', y: 45 },
|
||||
{ name: 'magic_wand', icon: '🪄', y: 81 },
|
||||
{ name: 'magic_wand', icon: '☯️', y: 81 },
|
||||
{ name: 'brush', icon: '🖌️', y: 117 }
|
||||
];
|
||||
|
||||
@@ -93,6 +94,7 @@ export class Toolbar {
|
||||
this.removeBgManager = new RemoveBgManager(this.node, this.maskManager);
|
||||
this.magicWandManager = new MagicWandManager(this.node);
|
||||
this.brushManager = new BrushManager(this.node);
|
||||
this.maskPainterManager = new MaskPainterManager(this.node);
|
||||
|
||||
this.setupColorPicker();
|
||||
this.createContextualToolbar();
|
||||
@@ -288,6 +290,7 @@ handleClick(e, mouseX, mouseY) {
|
||||
this.maskManager.hide();
|
||||
this.magicWandManager.hide();
|
||||
this.brushManager.hide();
|
||||
this.maskPainterManager.hide();
|
||||
if (this.selectionSubMenu) {
|
||||
this.selectionSubMenu.style.display = 'none';
|
||||
}
|
||||
@@ -536,15 +539,15 @@ createSelectionSubMenu() {
|
||||
this.selectionSubMenu.style.display = 'none';
|
||||
};
|
||||
|
||||
const rectButton = createEmojiButton("🔲", "Sélection Rectangle");
|
||||
rectButton.onclick = () => {
|
||||
console.log("Outil Rectangle sélectionné ! (non implémenté)");
|
||||
this.activeTool = 'rectangle_select';
|
||||
this.selectionSubMenu.style.display = 'none';
|
||||
|
||||
};
|
||||
const paintMaskButton = createEmojiButton("🖌️", "Paint Mask");
|
||||
|
||||
this.selectionSubMenu.append(wandButton, rectButton);
|
||||
if (this.maskPainterManager) {
|
||||
this.maskPainterManager.show();
|
||||
}
|
||||
this.selectionSubMenu.style.display = 'none';
|
||||
};
|
||||
|
||||
this.selectionSubMenu.append(wandButton, paintMaskButton);
|
||||
document.body.appendChild(this.selectionSubMenu);
|
||||
}
|
||||
|
||||
|
||||
+44
-5
@@ -676,18 +676,57 @@ async def refresh_previews_route(request):
|
||||
post_data = await request.json()
|
||||
properties_json = post_data.get("properties_json")
|
||||
|
||||
# On crée une instance temporaire de la classe LayerSystem
|
||||
layer_system_instance = LayerSystem()
|
||||
|
||||
# On appelle la fonction de compositing pour obtenir les données de l'UI (qui contiennent les URL des previews)
|
||||
# Mais on ignore le résultat de l'image, car on ne fait pas de rendu IA
|
||||
ui_data = layer_system_instance.composite_layers(_properties_json=properties_json)
|
||||
|
||||
# On renvoie uniquement les données de l'UI au frontend
|
||||
return web.json_response(ui_data.get("ui", {}))
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Layer System] ERREUR API refresh_previews: {e}")
|
||||
return web.Response(status=500, text=str(e))
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
@server.PromptServer.instance.routes.post("/layersystem/finalize_painter_mask")
|
||||
async def finalize_painter_mask_route(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
temp_alpha_mask_details = data.get("alpha_mask_details")
|
||||
layer_index = data.get("layer_index")
|
||||
|
||||
if not temp_alpha_mask_details or layer_index is None:
|
||||
return web.Response(status=400, text="Données manquantes")
|
||||
|
||||
alpha_mask_path = folder_paths.get_annotated_filepath(temp_alpha_mask_details["name"])
|
||||
alpha_mask_pil = Image.open(alpha_mask_path)
|
||||
alpha_channel = alpha_mask_pil.getchannel('A')
|
||||
|
||||
preview_mask_pil = Image.new("RGB", alpha_channel.size, "black")
|
||||
preview_mask_pil.paste((255, 255, 255), mask=alpha_channel)
|
||||
|
||||
render_mask_pil = ImageOps.invert(preview_mask_pil.convert("L")).convert("RGB")
|
||||
|
||||
output_dir = folder_paths.get_input_directory()
|
||||
|
||||
preview_filename = f"internal_mask_preview_{layer_index}.png"
|
||||
render_filename = f"internal_mask_render_{layer_index}.png"
|
||||
|
||||
preview_mask_pil.save(os.path.join(output_dir, preview_filename), "PNG")
|
||||
render_mask_pil.save(os.path.join(output_dir, render_filename), "PNG")
|
||||
|
||||
if os.path.exists(alpha_mask_path):
|
||||
os.remove(alpha_mask_path)
|
||||
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"preview_mask_details": { "name": preview_filename, "subfolder": "", "type": "input" },
|
||||
"render_mask_details": { "name": render_filename, "subfolder": "", "type": "input" }
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[Layer System] ERREUR API finalize_painter_mask: {e}")
|
||||
traceback.print_exc()
|
||||
return web.Response(status=500, text=str(e))
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = { "LayerSystem": LayerSystem }
|
||||
|
||||
Reference in New Issue
Block a user