diff --git a/js/brush_manager.js b/js/brush_manager.js index e828215..b0e826b 100644 --- a/js/brush_manager.js +++ b/js/brush_manager.js @@ -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')); diff --git a/js/layer_system_final.js b/js/layer_system_final.js index 351bf49..0fedf36 100644 --- a/js/layer_system_final.js +++ b/js/layer_system_final.js @@ -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); diff --git a/js/magic_wand_manager.js b/js/magic_wand_manager.js index fd00be6..7035db6 100644 --- a/js/magic_wand_manager.js +++ b/js/magic_wand_manager.js @@ -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)); diff --git a/js/mask_manager.js b/js/mask_manager.js index d2a4cf8..a06366d 100644 --- a/js/mask_manager.js +++ b/js/mask_manager.js @@ -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 => { diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js new file mode 100644 index 0000000..4dded1c --- /dev/null +++ b/js/mask_painter_manager.js @@ -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 = ` + + + ${this.settings.size} + + + ${this.settings.hardness}% + + + ${this.settings.opacity}% + + + + + `; + 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`; + } +} \ No newline at end of file diff --git a/js/removebg.js b/js/removebg.js index 0156e4d..d9f9279 100644 --- a/js/removebg.js +++ b/js/removebg.js @@ -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; diff --git a/js/toolbar.js b/js/toolbar.js index b97e771..42bf47d 100644 --- a/js/toolbar.js +++ b/js/toolbar.js @@ -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); } diff --git a/layer_system_final.py b/layer_system_final.py index 3ea3f7b..9ef5962 100644 --- a/layer_system_final.py +++ b/layer_system_final.py @@ -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 }