From 5ae65de57b85fba05aeb2753f4dccf2320245746 Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 04:35:44 +0200 Subject: [PATCH 01/20] add mask painter --- layer_system_final.py | 48 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/layer_system_final.py b/layer_system_final.py index 3ea3f7b..56ac4f8 100644 --- a/layer_system_final.py +++ b/layer_system_final.py @@ -687,7 +687,53 @@ async def refresh_previews_route(request): 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') + + # --- DÉBUT DE LA CORRECTION --- + + # 1. Crée le masque d'aperçu (Blanc = Visible) en format RGB + preview_mask_pil = Image.new("RGB", alpha_channel.size, "black") + preview_mask_pil.paste((255, 255, 255), mask=alpha_channel) + + # 2. Crée le masque de rendu (Noir = Visible) en format RGB, comme les autres outils + render_mask_pil = ImageOps.invert(preview_mask_pil.convert("L")).convert("RGB") + + # --- FIN DE LA CORRECTION --- + + output_dir = folder_paths.get_input_directory() + # On ajoute un timestamp pour éviter les problèmes de cache navigateur + timestamp = int(time.time() * 1000) + preview_filename = f"internal_mask_preview_{layer_index}_{timestamp}.png" + render_filename = f"internal_mask_render_{layer_index}_{timestamp}.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") + + 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 } From cdac2989a2681d70d55e7eee99163602de73a12e Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 04:36:43 +0200 Subject: [PATCH 02/20] Add mask painter --- js/layer_system_final.js | 7 + js/mask_painter_manager.js | 393 +++++++++++++++++++++++++++++++++++++ js/toolbar.js | 22 ++- 3 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 js/mask_painter_manager.js diff --git a/js/layer_system_final.js b/js/layer_system_final.js index 351bf49..161f331 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(); } }; @@ -439,6 +445,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_"); diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js new file mode 100644 index 0000000..53b8904 --- /dev/null +++ b/js/mask_painter_manager.js @@ -0,0 +1,393 @@ +export class MaskPainterManager { + constructor(node) { + this.node = node; + this.isDrawing = false; + this.lastPoint = { x: 0, y: 0 }; + this.settings = { + size: 30, + hardness: 80, // Le retour de la dureté ! + 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(); + this.toolbar = document.createElement("div"); + 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: '8px', color: 'white', fontFamily: 'sans-serif' + }); + + this.toolbar.innerHTML = ` + + + ${this.settings.size} + + + ${this.settings.hardness}% + + + + + `; + document.body.appendChild(this.toolbar); + + 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' ? '%' : ''; + 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.querySelector('[data-mode="brush"]').style.border = `2px solid ${this.settings.mode === 'brush' ? 'white' : 'gray'}`; + this.toolbar.querySelector('[data-mode="eraser"]').style.border = `2px solid ${this.settings.mode === 'eraser' ? 'white' : 'gray'}`; + } + if (target.dataset.action === 'apply') this.finalizeDrawing(); + if (target.dataset.action === 'cancel') this.hide(); + }); + } + +async show() { + 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; + + // On éteint le calque dans le rendu principal + 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) { + // CAS 1 : Masque de l'éditeur (RGBA). Ton test prouve qu'il faut inverser sa transparence. + for (let i = 0; i < data.length; i += 4) { + data[i + 3] = 255 - data[i + 3]; // On inverse le canal alpha existant + data[i] = 255; data[i + 1] = 255; data[i + 2] = 255; // Et on normalise le RGB + } + } else { + // CAS 2 : Masque de notre outil (RGB Noir/Blanc). Cette logique est correcte et n'inverse pas. + for (let i = 0; i < data.length; i += 4) { + data[i + 3] = 255 - data[i]; // alpha = 255 - luminance + data[i] = data[i+1] = data[i+2] = 255; + } + } + this.maskCtx.putImageData(imageData, 0, 0); + + } else { + // Un nouveau masque est entièrement opaque (calque visible). + 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); + // La dureté (0-100) contrôle où le dégradé devient transparent + const hardnessStop = Math.max(0, Math.min(1, this.settings.hardness / 100)); + + // Pinceau (Révéler) -> Peint de l'opaque avec 'source-over' + if (this.settings.mode === 'brush') { + gradient.addColorStop(0, 'white'); + gradient.addColorStop(hardnessStop, 'white'); + gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); + ctx.globalCompositeOperation = 'source-over'; + ctx.fillStyle = gradient; + + // Gomme (Cacher) -> Efface vers le transparent avec 'destination-out' + } else { + gradient.addColorStop(0, 'black'); // La couleur n'importe pas + 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.globalCompositeOperation = 'source-over'; + } + + hide() { + if (this.activeLayer) { + const props = this.node.layer_properties[this.activeLayer.name]; + if (props) { + props.enabled = true; // On rallume le calque + 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 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"); + + // --- CORRECTION --- + // On sauvegarde le fichier temporaire dans 'input' au lieu de 'temp' + formData.append("type", "input"); + // --- FIN CORRECTION --- + + 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; + + // --- DÉBUT DE LA CORRECTION --- + // On calcule la position du clic par rapport au canvas principal en dessous + 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 sur la barre d'outils principale, on lui passe l'événement et on s'arrête. + if (e.type === 'mousedown' && this.node.toolbar.isClickOnToolbar(clickX_on_main_canvas, clickY_on_main_canvas)) { + this.node.toolbar.handleClick(e, clickX_on_main_canvas, clickY_on_main_canvas); + return; + } + // --- FIN DE LA CORRECTION --- + + 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); + 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); + + // On ne dessine plus le fond, il est déjà visible en dessous ! + + 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/toolbar.js b/js/toolbar.js index b97e771..989ee58 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,16 @@ 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"); // NOUVEAU +paintMaskButton.onclick = () => { + this.activeTool = 'mask_painter'; + if (this.maskPainterManager) { + this.maskPainterManager.show(); + } + this.selectionSubMenu.style.display = 'none'; +}; - this.selectionSubMenu.append(wandButton, rectButton); + this.selectionSubMenu.append(wandButton, paintMaskButton); document.body.appendChild(this.selectionSubMenu); } From 12029fa0722a4343ab0b56e94cf392c2a9d6182e Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 12:24:55 +0200 Subject: [PATCH 03/20] fix change layers --- layer_system_final.js | 1788 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1788 insertions(+) create mode 100644 layer_system_final.js diff --git a/layer_system_final.js b/layer_system_final.js new file mode 100644 index 0000000..f7519b0 --- /dev/null +++ b/layer_system_final.js @@ -0,0 +1,1788 @@ +import { app } from "/scripts/app.js"; +import { Toolbar } from './toolbar.js'; +function applyMask(layerImage, maskImage) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + canvas.width = layerImage.width; + canvas.height = layerImage.height; + ctx.drawImage(maskImage, 0, 0, layerImage.width, layerImage.height); + const maskData = ctx.getImageData(0, 0, canvas.width, canvas.height); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(layerImage, 0, 0); + const layerData = ctx.getImageData(0, 0, canvas.width, canvas.height); + for (let i = 0; i < maskData.data.length; i += 4) { + const luminance = maskData.data[i]; + layerData.data[i + 3] = luminance; + } + ctx.putImageData(layerData, 0, 0); + return canvas; +} +const BLEND_MODES = ["normal", "multiply", "screen", "overlay", "soft-light", "hard-light", "difference", "color-dodge", "color-burn"]; +const RESIZE_MODES = ["stretch", "fit", "cover", "crop"]; +const MAX_LAYERS = 11; +const rotateCursorSVG = ``; +const rotateCursorDataUri = `data:image/svg+xml;base64,${btoa(rotateCursorSVG)}`; +const rotateCursorStyle = `url(${rotateCursorDataUri}) 12 12, auto`; +const eyeIconPath = new Path2D("M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zM12 9c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"); +const eyeSlashPath = new Path2D("M2 4.27l2.28 2.28L3.27 7.5C1.94 8.85 1 10.34 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l2.12 2.12L19.73 19 2 4.27zM12 17c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.01.19-.02.38-.02.57 0 1.66 1.34 3 3 3 .19 0 .38-.01.57-.02l1.57 1.57C13.5 16.82 12.77 17 12 17zm7.88-8.13C21.06 10.15 22 11.08 23 12c-1.73 4.39-6 7.5-11 7.5-.94 0-1.84-.13-2.69-.36l2.03 2.03c.85.22 1.74.33 2.66.33 5 0 9.27-3.11 11-7.5-.73-1.83-2.1-3.38-3.73-4.54l-1.39 1.39zM12 9c.99 0 1.89.28 2.67.77l-1.1-1.1C13.04 8.28 12.53 8 12 8c-2.76 0-5 2.24-5 5 0 .53.08 1.04.23 1.53l-1.1-1.1c-.49-.78-.73-1.68-.73-2.63 0-2.76 2.24-5 5-5z"); +const lockIconPath = new Path2D("M17 8v-1a5 5 0 00-10 0v1H5v12h14V8h-2zm-5 7a2 2 0 110-4 2 2 0 010 4zM9 7V6a3 3 0 116 0v1H9z"); +const unlockIconPath = new Path2D("M9 7V6a3 3 0 116 0v1h2V6a5 5 0 00-10 0v1H5v12h14V8H9zm3 9a2 2 0 110-4 2 2 0 010 4z"); +const arrowUpPath = new Path2D("M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"); +const arrowDownPath = new Path2D("M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"); +const moveIconPath = new Path2D("M12 2 L12 22 M2 12 L22 12 M12 2 L8 6 M12 2 L16 6 M12 22 L8 18 M12 22 L16 18 M2 12 L6 8 M2 12 L6 16 M22 12 L18 8 M22 12 L18 16"); +const trashIconPath = new Path2D("M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"); +const replaceIconPath = new Path2D("M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"); +app.registerExtension({ + name: "LayerSystem.DynamicLayers", + + beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "LayerSystem") { + return; + } + const resizeHeight = function() { + if (!this.size) return; + const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); + if (anchorWidget && this.basePreviewImage) { + const aspectRatio = this.basePreviewImage.naturalHeight / this.basePreviewImage.naturalWidth; + const toolbarWidth = this.toolbar ? this.toolbar.width : 0; + const contentWidth = this.size[0] - 20 - toolbarWidth; + const requiredHeight = contentWidth * aspectRatio; + + anchorWidget.computeSize = () => [this.size[0], requiredHeight]; + } + const newSize = this.computeSize(); + this.size[1] = newSize[1]; + this.onResize?.(this.size); + this.graph.setDirtyCanvas(true, true); + if (anchorWidget?.computeSize) { + delete anchorWidget.computeSize; + } + }; + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function(message) { + onExecuted?.apply(this, arguments); + if (message?.layer_previews && message.layer_previews[0]) { + const previewData = message.layer_previews[0]; + this.preview_data = previewData; + const imagePromises = Object.entries(previewData).map(([name, previewInfo]) => { + const url = previewInfo.url; + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = "anonymous"; + if (url) { + img.src = url + `?t=${Date.now()}`; + img.onload = () => resolve({ name, img }); + img.onerror = (err) => reject(err); + } else { + reject(new Error(`[Layer System] Missing preview URL for ${name}`)); + } + }); + }); + Promise.all(imagePromises) + .then(loadedImages => { + this.loaded_preview_images = loadedImages.reduce((acc, {name, img}) => { + acc[name] = img; + return acc; + }, {}); + this.basePreviewImage = this.loaded_preview_images.base_image; + resizeHeight.call(this); + this.redrawPreviewCanvas(); + this.refreshUI(); + }) + .catch(e => console.error("[Layer System] At least one preview image could not be loaded.", e)); + } + }; + + const onDrawBackground = nodeType.prototype.onDrawBackground; + nodeType.prototype.onDrawBackground = function(ctx) { + 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') { + this.toolbar.maskManager.positionToolbar(); + } + if (this.toolbar.contextualToolbar?.style.display !== 'none') { + this.toolbar.updateContextualToolbarPosition(); + } + if (this.toolbar.magicWandManager?.contextualToolbar?.style.display !== 'none') { + this.toolbar.magicWandManager.positionToolbar(); + } + if (this.toolbar.selectionSubMenu?.style.display !== 'none') { + this.toolbar.positionSelectionSubMenu(); + } + } +}; + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + this.base_image_properties = null; + this.layer_properties = this.layer_properties || {}; + + const onRemoved_original = this.onRemoved; + this.onRemoved = () => { + onRemoved_original?.apply(this, arguments); + if (this.toolbar?.contextualToolbar) { + this.toolbar.contextualToolbar.remove(); + } + if (this.toolbar?.maskManager?.contextualToolbar) { + this.toolbar.maskManager.contextualToolbar.remove(); + } + if (this.toolbar.selectionSubMenu) { + this.toolbar.selectionSubMenu.remove(); + } + if (this.toolbar.magicWandManager?.settingsToolbar) { + this.toolbar.magicWandManager.settingsToolbar.remove(); + } + if (this.toolbar?.brushManager?.toolbar) { + this.toolbar.brushManager.toolbar.remove(); + } + if (this.toolbar?.maskPainterManager?.toolbar) { + this.toolbar.maskPainterManager.toolbar.remove(); + } + }; + +const topSpacer = { + name: "global_top_spacer", + type: "CUSTOM_SPACER", + draw: () => {}, + computeSize: () => [0, 10] +}; +this.widgets.push(topSpacer); + this.addWidget( + "button", + "Add Image", + null, + () => { this.handleInternalImageLoad(); } + ); + + this.basePreviewImage = null; + this.preview_data = {}; + this.loaded_preview_images = {}; + this.redraw_req = null; + this.previewCanvasScale = 1.0; + this.movingLayer = null; + this.isDragging = false; + this.dragStart = { x: 0, y: 0 }; + this.initialOffsets = { x: 0, y: 0 }; + this.accordionMode = true; + this.interactionMode = "none"; + this.movingLayerBounds = null; + this.initialScale = 1.0; + this.initialBounds = null; + this.layerAspectRatio = 1.0; + this.initialRotation = 0.0; + this.textActionMode = "none"; + this.activeTextObject = null; + this.initialTextPos = { x: 0, y: 0 }; + this.isTextDragging = false; + this.dragOffset = { x: 0, y: 0 }; + this.needsFirstSync = true; + this._resizing = false; + this.rotationOffsetAngle = 0.0; + + this.toolbar = new Toolbar(this); + this.size[0] = 800; + + setTimeout(() => { + const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); + if (anchorWidget && anchorWidget.inputEl) { + const canvas = document.createElement("canvas"); + const container = anchorWidget.inputEl.parentElement; + container.style.padding = "0px"; + container.style.margin = "0px"; + anchorWidget.inputEl.style.display = "none"; + container.appendChild(canvas); + this.previewCanvas = canvas; + this.previewCtx = canvas.getContext("2d"); + + const overlayCanvas = document.createElement("canvas"); + overlayCanvas.id = "ls-overlay-canvas"; + overlayCanvas.style.position = "absolute"; + overlayCanvas.style.top = "0"; + overlayCanvas.style.left = "0"; + overlayCanvas.style.pointerEvents = "none"; + container.appendChild(overlayCanvas); + this.overlayCanvas = overlayCanvas; + + const anchorIndex = this.widgets.indexOf(anchorWidget); + if (anchorIndex > 0) { + this.widgets.splice(anchorIndex, 1); + this.widgets.unshift(anchorWidget); + } + this.redrawPreviewCanvas(); +this.previewCanvas.addEventListener('mousedown', (e) => { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + + if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { + this.toolbar.handleClick(e, mouseX, mouseY); + return; + } + + if (this.textActionMode === 'moving' && this.activeTextObject) { + const targetText = this.findTextElementAtPos(mouseX, mouseY); + if (targetText && targetText.id === this.activeTextObject.id) { + this.dragOffset = { + x: targetText.x - mouseX, + y: targetText.y - mouseY + }; + this.isTextDragging = true; + } + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (this.toolbar.activeTool === 'magic_wand' && this.toolbar.magicWandManager) { + this.toolbar.magicWandManager.handleCanvasClick(e); + return; + } + if (this.toolbar.activeTool) { + this.toolbar.handleCanvasClick(e); + return; + } + + if (!this.movingLayer) return; + const props = this.layer_properties[this.movingLayer]; + if (!props) return; + const handle = this.getHandleAtPos(e); + if (handle && handle !== 'rotate') { + this.interactionMode = "scaling_" + handle; + this.dragStart = { x: e.clientX, y: e.clientY }; + this.initialScale = props.scale; + this.initialOffsets = { x: props.offset_x, y: props.offset_y }; + this.initialBounds = { ...this.movingLayerBounds }; + this.layerAspectRatio = this.initialBounds.w / this.initialBounds.h; + } else if (handle === 'rotate') { + this.interactionMode = "rotating"; + const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; + const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; + this.dragStartAngle = Math.atan2(mouseY - centerY, mouseX - centerX); + this.initialRotation = props.rotation || 0; + } else { + this.interactionMode = "moving"; + this.isDragging = true; + this.dragStart = { x: e.clientX, y: e.clientY }; + this.initialOffsets = { x: props.offset_x, y: props.offset_y }; + } + e.preventDefault(); + e.stopPropagation(); +}); + this.previewCanvas.addEventListener('mousemove', this.onCanvasMouseMove.bind(this)); + this.previewCanvas.addEventListener('mouseup', this.onCanvasMouseUp.bind(this)); + this.previewCanvas.addEventListener('mouseleave', this.onCanvasMouseLeave.bind(this)); + } + this.initializeHeaderCanvases(); + + const p_widget = this.widgets.find(w => w.name === "_properties_json"); + if(p_widget) { + p_widget.hidden = true; + p_widget.computeSize = () => [0, -4]; + } + if (this.loadedConfig) { + this.loadStateFromConfig(this.loadedConfig); +} else { + this.refreshUI(); +} + }, 0); + }; + +nodeType.prototype.onResize = function(size) { + if (this._resizing) return; + this._resizing = true; + if (!this.widgets || !this.size || !this.basePreviewImage || this.basePreviewImage.naturalWidth <= 0) { + this._resizing = false; + return; + } + + const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); + if (anchorWidget) { + const aspectRatio = this.basePreviewImage.naturalHeight / this.basePreviewImage.naturalWidth; + const toolbarWidth = this.toolbar ? this.toolbar.width : 0; + const contentWidth = size[0] - 20 - toolbarWidth; + const requiredHeight = contentWidth * aspectRatio; + anchorWidget.computeSize = () => [size[0], requiredHeight]; + } + + const newComputedSize = this.computeSize(); + this.size[1] = newComputedSize[1]; + if (anchorWidget?.computeSize) { + delete anchorWidget.computeSize; + } + + if (this.previewCanvas) { + if (this.redraw_req) cancelAnimationFrame(this.redraw_req); + this.redraw_req = requestAnimationFrame(() => { + this.redrawPreviewCanvas(); + this.redraw_req = null; + }); + } + + if (this.base_image_properties) { + const baseHeaderAnchor = this.widgets.find(w => w.name === 'header_anchor_1'); + if (baseHeaderAnchor && baseHeaderAnchor.canvas) { + this.drawHeaderCanvas(baseHeaderAnchor.canvas, 'base_image'); + } + } + + const activeLayerKeys = Object.keys(this.layer_properties); + for (const layerName of activeLayerKeys) { + const layerIndex = parseInt(layerName.split('_')[1]); + const headerAnchor = this.widgets.find(w => w.name === `header_anchor_${layerIndex + 1}`); + if (headerAnchor && headerAnchor.canvas) { + this.drawHeaderCanvas(headerAnchor.canvas, layerName); + } + } + this._resizing = false; +}; + +nodeType.prototype.onConfigure = function(info) { + const onConfigureOriginal = nodeType.prototype.__proto__.onConfigure; + onConfigureOriginal?.apply(this, arguments); + this.loadedConfig = info; +}; + +nodeType.prototype.loadStateFromConfig = function(info) { + if (this.stateLoaded) return; + if (info.widgets_values) { + const jsonWidgetIndex = this.widgets.findIndex(w => w.name === "_properties_json"); + if (jsonWidgetIndex > -1 && info.widgets_values[jsonWidgetIndex]) { + try { + 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); + this.refreshUI(); + + } catch (e) { + console.error("[Layer System] Error loading JSON state", e); + this.refreshUI(); + } + } + } else { + this.refreshUI(); + } +}; + +nodeType.prototype.refreshPreviewsOnly = async function() { + const properties_widget = this.widgets.find(w => w.name === "_properties_json"); + if (!properties_widget) return; + + try { + const response = await fetch("/layersystem/refresh_previews", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ properties_json: properties_widget.value }), + }); + + if (!response.ok) { + throw new Error(`Server error: ${response.status}`); + } + + 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 }); + } + } catch (e) { + console.error("[Layer System] Failed to refresh previews:", e); + } +}; + + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (side, slot, is_connected, link_info, io_slot) { + onConnectionsChange?.apply(this, arguments); + if (side === 1) { setTimeout(() => this.refreshUI(), 0); } + }; + + nodeType.prototype.redrawPreviewCanvas = function() { + if (!this.previewCanvas || !this.size || !this.basePreviewImage || !this.basePreviewImage.naturalWidth) { + if(this.previewCtx) this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); + return; + } + + const canvas = this.previewCanvas; + const ctx = this.previewCtx; + const baseImg = this.basePreviewImage; + const availableWidth = this.size[0] - 20; + const toolbarWidth = this.toolbar.width; + const imageAreaWidth = availableWidth - toolbarWidth; + const aspectRatio = baseImg.naturalHeight / baseImg.naturalWidth; + const requiredHeight = imageAreaWidth * aspectRatio; + if (canvas.width !== availableWidth) { + canvas.width = availableWidth; + } + const finalHeight = Math.round(requiredHeight); + if (canvas.height !== finalHeight) { + canvas.height = finalHeight; + } + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const destX = toolbarWidth; + const destY = 0; + const destWidth = imageAreaWidth; + const destHeight = finalHeight; + ctx.drawImage(baseImg, destX, destY, destWidth, destHeight); + this.previewCanvasScale = destWidth / baseImg.naturalWidth; + + 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_"); + const maskImage = this.loaded_preview_images[maskName]; + if (!props || !props.enabled || !layerImage || !layerImage.naturalWidth || !layerImage.naturalHeight) { + continue; + } + ctx.save(); + let imageToDraw = layerImage; + if (props.brightness !== 0.0 || props.contrast !== 0.0 || props.saturation !== 1.0 || props.color_r !== 1.0 || props.color_g !== 1.0 || props.color_b !== 1.0) { + try { + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = layerImage.naturalWidth; tempCanvas.height = layerImage.naturalHeight; + const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); + tempCtx.drawImage(layerImage, 0, 0); + const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + const data = imageData.data; + const brightness = props.brightness, contrastFactor = 1.0 + props.contrast, saturation = props.saturation, color_r = props.color_r, color_g = props.color_g, color_b = props.color_b; + for (let i = 0; i < data.length; i += 4) { + let r = data[i] / 255.0, g = data[i + 1] / 255.0, b = data[i + 2] / 255.0; + if (brightness !== 0.0) { r += brightness; g += brightness; b += brightness; } + if (props.contrast !== 0.0) { r = (r - 0.5) * contrastFactor + 0.5; g = (g - 0.5) * contrastFactor + 0.5; b = (b - 0.5) * contrastFactor + 0.5; } + if (color_r !== 1.0 || color_g !== 1.0 || color_b !== 1.0) { r *= color_r; g *= color_g; b *= color_b; } + if (saturation !== 1.0) { const gray = r * 0.299 + g * 0.587 + b * 0.114; r = gray * (1.0 - saturation) + r * saturation; g = gray * (1.0 - saturation) + g * saturation; b = gray * (1.0 - saturation) + b * saturation; } + data[i] = Math.max(0, Math.min(255, r * 255)); data[i + 1] = Math.max(0, Math.min(255, g * 255)); data[i + 2] = Math.max(0, Math.min(255, b * 255)); + } + tempCtx.putImageData(imageData, 0, 0); + imageToDraw = tempCanvas; + } catch (e) { + console.error(`[LayerSystem] ERREUR lors du traitement du calque ${layerName}:`, e); + imageToDraw = layerImage; + } + } + let finalImageToDraw = imageToDraw; + if (maskImage && maskImage.naturalWidth > 0) { + let maskToApply = maskImage; + if (props.invert_mask) { + const invertedMaskCanvas = document.createElement('canvas'); + invertedMaskCanvas.width = maskImage.naturalWidth; + invertedMaskCanvas.height = maskImage.naturalHeight; + const invertedCtx = invertedMaskCanvas.getContext('2d'); + invertedCtx.filter = 'invert(1)'; + invertedCtx.drawImage(maskImage, 0, 0); + maskToApply = invertedMaskCanvas; + } + finalImageToDraw = applyMask(imageToDraw, maskToApply); + } + let final_sx = 0, final_sy = 0, final_sw = finalImageToDraw.width, final_sh = finalImageToDraw.height; + let final_dx = destX, final_dy = destY, final_dw = destWidth, final_dh = destHeight; + + if (props.resize_mode === 'crop') { + final_dw = final_sw * props.scale * this.previewCanvasScale; + final_dh = final_sh * props.scale * this.previewCanvasScale; + final_dx = destX + (props.offset_x * this.previewCanvasScale) - final_dw/2 + destWidth/2; + final_dy = destY + (props.offset_y * this.previewCanvasScale) - final_dh/2 + destHeight/2; + } else { + const layerRatio = final_sw / final_sh; + const destContainerRatio = destWidth / destHeight; + switch(props.resize_mode) { + case 'fit': + if (layerRatio > destContainerRatio) { final_dh = final_dw / layerRatio; final_dy += (destHeight - final_dh) / 2; } + else { final_dw = final_dh * layerRatio; final_dx += (destWidth - final_dw) / 2; } + break; + case 'cover': + if (layerRatio > destContainerRatio) { + final_sw = final_sh * destContainerRatio; + final_sx = (finalImageToDraw.width - final_sw) / 2; + } else { + final_sh = final_sw / destContainerRatio; + final_sy = (finalImageToDraw.height - final_sh) / 2; + } + break; + case 'stretch': default: break; + } + } + + ctx.globalAlpha = props.opacity; + ctx.globalCompositeOperation = props.blend_mode === 'normal' ? 'source-over' : props.blend_mode; + if (props.resize_mode === 'crop') { + const centerX = final_dx + final_dw / 2; + const centerY = final_dy + final_dh / 2; + + ctx.save(); + ctx.translate(centerX, centerY); + const angleInRadians = (props.rotation || 0) * Math.PI / 180; + ctx.rotate(angleInRadians); + ctx.drawImage(finalImageToDraw, final_sx, final_sy, final_sw, final_sh, -final_dw / 2, -final_dh / 2, final_dw, final_dh); + if (this.movingLayer === layerName) { + ctx.strokeStyle = "red"; + ctx.lineWidth = 2; + ctx.strokeRect(-final_dw / 2, -final_dh / 2, final_dw, final_dh); + + this.movingLayerBounds = { x: final_dx, y: final_dy, w: final_dw, h: final_dh, center_x: centerX, center_y: centerY }; + + const handleSize = 8; + ctx.fillStyle = "white"; + ctx.strokeStyle = "black"; + ctx.lineWidth = 1; + const corners = [ + {x: -final_dw/2, y: -final_dh/2}, {x: final_dw/2, y: -final_dh/2}, + {x: -final_dw/2, y: final_dh/2}, {x: final_dw/2, y: final_dh/2} + ]; + for(const corner of corners) { + ctx.fillRect(corner.x - handleSize/2, corner.y - handleSize/2, handleSize, handleSize); + ctx.strokeRect(corner.x - handleSize/2, corner.y - handleSize/2, handleSize, handleSize); + } + + const rotHandleOffset = 20; + const rotHandleY = -final_dh/2 - rotHandleOffset; + + ctx.beginPath(); + ctx.moveTo(0, -final_dh/2); + ctx.lineTo(0, rotHandleY); + ctx.strokeStyle = "red"; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, rotHandleY, handleSize / 2, 0, 2 * Math.PI); + ctx.fillStyle = "white"; + ctx.fill(); + ctx.strokeStyle = "black"; + ctx.lineWidth = 1; + ctx.stroke(); + } + + ctx.restore(); + } else { + ctx.drawImage(finalImageToDraw, final_sx, final_sy, final_sw, final_sh, final_dx, final_dy, final_dw, final_dh); + } + + ctx.restore(); + } + + if (this.toolbar && this.toolbar.selectedTextObject) { + const el = this.toolbar.selectedTextObject; + const metrics = this.getTextPreviewMetrics(el); + if (metrics) { + ctx.save(); + ctx.strokeStyle = '#FFD700'; + ctx.lineWidth = 2; + ctx.setLineDash([6, 3]); + + const padding = 5; + ctx.strokeRect( + metrics.x - padding, + metrics.y - padding, + metrics.width + (padding * 2), + metrics.height + (padding * 2) + ); + + ctx.restore(); + } + } + + this.toolbar.drawTextElements(ctx); + this.toolbar.draw(ctx); + }; + +nodeType.prototype.getTextPreviewMetrics = function(textEl) { + if (!this.basePreviewImage || !this.previewCanvas || !this.toolbar) { return null; } + const baseImageWidth = this.basePreviewImage.naturalWidth; + const previewAreaWidth = this.previewCanvas.width - this.toolbar.width; + if (baseImageWidth <= 0) return null; + const inverseRatio = previewAreaWidth / baseImageWidth; + const previewCenterX = previewAreaWidth / 2; + const previewCenterY = this.previewCanvas.height / 2; + const preview_size = textEl.size * inverseRatio; + const preview_offset_x = textEl.offset_x * inverseRatio; + const preview_offset_y = textEl.offset_y * inverseRatio; + const preview_x = this.toolbar.width + previewCenterX + preview_offset_x; + const preview_y = previewCenterY + preview_offset_y; + const ctx = this.previewCtx; + ctx.save(); + ctx.font = `${preview_size}px ${textEl.fontFamily || 'Arial'}`; + const metrics = ctx.measureText(textEl.text); + ctx.restore(); + const textHeight = (metrics.fontBoundingBoxAscent || 0) + (metrics.fontBoundingBoxDescent || 0); + const finalHeight = textHeight > 0 ? textHeight : preview_size; + return { + x: preview_x, + y: preview_y, + size: preview_size, + width: metrics.width, + height: finalHeight -10 + }; +}; +nodeType.prototype.findTextElementAtPos = function(mouseX, mouseY) { + if (!this.toolbar || !this.toolbar.textElements.length || !this.previewCanvas || !this.basePreviewImage) { + return null; + } + const ctx = this.previewCtx; + const baseImageWidth = this.basePreviewImage.naturalWidth; + const previewAreaWidth = this.previewCanvas.width - this.toolbar.width; + + if (baseImageWidth <= 0) return null; + const inverseRatio = previewAreaWidth / baseImageWidth; + const previewCenterX = previewAreaWidth / 2; + const previewCenterY = this.previewCanvas.height / 2; + ctx.save(); + + for (let i = this.toolbar.textElements.length - 1; i >= 0; i--) { + const textEl = this.toolbar.textElements[i]; + const preview_offset_x = textEl.offset_x * inverseRatio; + const preview_offset_y = textEl.offset_y * inverseRatio; + const preview_size = textEl.size * inverseRatio; + const preview_x = this.toolbar.width + previewCenterX + preview_offset_x; + const preview_y = previewCenterY + preview_offset_y; + + ctx.font = `${preview_size}px ${textEl.fontFamily || 'Arial'}`; + const metrics = ctx.measureText(textEl.text); + const textWidth = metrics.width; + const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent || preview_size; + if (mouseX >= preview_x && mouseX <= preview_x + textWidth && + mouseY >= preview_y && mouseY <= preview_y + textHeight) + { + ctx.restore(); + return textEl; + } + } + ctx.restore(); + return null; +}; +nodeType.prototype.editTextElement = function(textElement) { + if (this.toolbar.activeTextarea) this.toolbar.activeTextarea.remove(); + const metrics = this.getTextPreviewMetrics(textElement); + if (!metrics) { + console.error("[Layer System] Unable to find text position for editing."); + return; + } + const canvasRect = this.previewCanvas.getBoundingClientRect(); + const finalLeft = canvasRect.left + metrics.x; + const finalTop = canvasRect.top + metrics.y; + const finalFontSize = metrics.size; + const textInput = document.createElement("div"); + this.toolbar.activeTextarea = textInput; + textInput.contentEditable = true; + textInput.innerText = textElement.text; + Object.assign(textInput.style, { + position: 'fixed', + left: `${finalLeft}px`, + top: `${finalTop}px`, + fontFamily: textElement.fontFamily, + fontSize: `${finalFontSize}px`, + color: 'white', + background: 'rgba(20,20,20,0.9)', + border: '1px solid #FFD700', + padding: '5px', + zIndex: '9999', + minWidth: `${metrics.width}px`, + whiteSpace: 'pre-wrap' + }); + document.body.appendChild(textInput); + textInput.focus(); + document.execCommand('selectAll', false, null); + textInput.addEventListener('keydown', (evt) => evt.stopPropagation()); + const onFinish = () => { + const newText = textInput.innerText; + + if (newText === "") { + const index = this.toolbar.textElements.findIndex(el => el.id === textElement.id); + if (index > -1) this.toolbar.textElements.splice(index, 1); + } else { + textElement.text = newText; + } + this.updatePropertiesJSON(); + this.redrawPreviewCanvas(); + + if (textInput.parentElement) textInput.parentElement.removeChild(textInput); + this.toolbar.activeTextarea = null; + this.toolbar.hideContextualToolbar(); + }; + textInput.addEventListener('blur', onFinish); + textInput.addEventListener('keydown', (evt) => { + evt.stopPropagation(); + if (evt.key === 'Enter' && !evt.shiftKey) { + evt.preventDefault(); + onFinish(); + } + if (evt.key === 'Escape') { + textInput.innerText = textElement.text; + onFinish(); + } + }); +}; +nodeType.prototype.showTextContextMenu = function(clientX, clientY, textElement) { + const existingMenu = document.getElementById("text-context-menu"); + if (existingMenu) existingMenu.remove(); + + this.activeTextObject = textElement; + const menu = document.createElement("div"); + menu.id = "text-context-menu"; + Object.assign(menu.style, { + position: 'fixed', left: `${clientX}px`, top: `${clientY}px`, backgroundColor: '#333', + border: '1px solid #555', borderRadius: '4px', padding: '5px', zIndex: '10000', + display: 'flex', flexDirection: 'column', + }); + const actionNames = ['Modifier le texte', 'Déplacer', 'Redimensionner', 'Pivoter', 'Supprimer']; + + actionNames.forEach(actionName => { + const button = document.createElement("button"); + button.innerText = actionName; + Object.assign(button.style, { + backgroundColor: '#444', color: 'white', border: '1px solid #666', + padding: '8px 12px', margin: '2px', textAlign: 'left', cursor: 'pointer' + }); + button.onmouseover = () => button.style.backgroundColor = '#555'; + button.onmouseout = () => button.style.backgroundColor = '#444'; + + button.onclick = (e) => { + menu.remove(); + const currentTextElement = this.toolbar.textElements.find(el => el.id === textElement.id); + if (!currentTextElement) { + console.log("L'élément texte a été supprimé, action annulée."); + return; + } + switch (actionName) { + case 'Modifier le texte': + this.editTextElement(currentTextElement); + break; + case 'Déplacer': + this.textActionMode = 'moving'; + this.activeTextObject = currentTextElement; + this.redrawPreviewCanvas(); + break; + case 'Supprimer': + const index = this.toolbar.textElements.findIndex(el => el.id === currentTextElement.id); + if (index > -1) { + this.toolbar.textElements.splice(index, 1); + this.updatePropertiesJSON(); + this.redrawPreviewCanvas(); + } + break; + } + }; + menu.appendChild(button); + }); + document.body.appendChild(menu); + const closeMenuHandler = (e) => { + if (!menu.contains(e.target)) { + menu.remove(); + document.removeEventListener('mousedown', closeMenuHandler, true); + } + }; + + setTimeout(() => document.addEventListener('mousedown', closeMenuHandler, true), 0); +}; +nodeType.prototype.onCanvasMouseDown = function(e) { + + + const mouseX = e.offsetX; + const mouseY = e.offsetY; + if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { + this.toolbar.handleClick(e, mouseX, mouseY); + return; + } + + + if (this.toolbar.activeTool) { + this.toolbar.handleCanvasClick(e); + return; + } + if (!this.movingLayer) { + return; + } + const props = this.layer_properties[this.movingLayer]; + if (!props) return; + const handle = this.getHandleAtPos(e); + if (handle && handle !== 'rotate') { + this.interactionMode = "scaling_" + handle; + this.dragStart = { x: e.clientX, y: e.clientY }; + this.initialScale = props.scale; + this.initialOffsets = { x: props.offset_x, y: props.offset_y }; + this.initialBounds = { ...this.movingLayerBounds }; + this.layerAspectRatio = this.initialBounds.w / this.initialBounds.h; +} else if (handle === 'rotate') { + this.interactionMode = "rotating"; + const props = this.layer_properties[this.movingLayer]; + const mouseX = e.offsetX; + const mouseY = e.offsetY; + const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; + const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; + this.dragStartAngle = Math.atan2(mouseY - centerY, mouseX - centerX); + this.initialRotation = props.rotation || 0; +} else { + this.interactionMode = "moving"; + this.isDragging = true; + this.dragStart = { x: e.clientX, y: e.clientY }; + this.initialOffsets = { x: props.offset_x, y: props.offset_y }; + } + + e.preventDefault(); + e.stopPropagation(); +}; +nodeType.prototype.getHandleAtPos = function(e) { + if (!this.movingLayer || !this.movingLayerBounds || !this.previewCanvas) return null; + + const mouseX = e.offsetX; + const mouseY = e.offsetY; + const handleSize = 12; + const bounds = this.movingLayerBounds; + + const props = this.layer_properties[this.movingLayer]; + const angleInRadians = (props.rotation || 0) * Math.PI / 180; + const centerX = bounds.x + bounds.w / 2; + const centerY = bounds.y + bounds.h / 2; + const cos = Math.cos(-angleInRadians); + const sin = Math.sin(-angleInRadians); + const dx = mouseX - centerX; + const dy = mouseY - centerY; + const rotatedMouseX = dx * cos - dy * sin + centerX; + const rotatedMouseY = dx * sin + dy * cos + centerY; + + const rotHandleSize = 16; + const rotHandleX_unrotated = bounds.x + bounds.w / 2; + const rotHandleY_unrotated = bounds.y - 20; + + if (rotatedMouseX >= rotHandleX_unrotated - rotHandleSize / 2 && rotatedMouseX <= rotHandleX_unrotated + rotHandleSize / 2 && + rotatedMouseY >= rotHandleY_unrotated - rotHandleSize / 2 && rotatedMouseY <= rotHandleY_unrotated + rotHandleSize / 2) { + return "rotate"; + } + const corners = { + tl: { x: bounds.x, y: bounds.y }, + tr: { x: bounds.x + bounds.w, y: bounds.y }, + bl: { x: bounds.x, y: bounds.y + bounds.h }, + br: { x: bounds.x + bounds.w, y: bounds.y + bounds.h } + }; + for (const key in corners) { + const corner = corners[key]; + const handleX = corner.x; + const handleY = corner.y; + if (rotatedMouseX >= handleX - handleSize/2 && rotatedMouseX <= handleX + handleSize/2 && + rotatedMouseY >= handleY - handleSize/2 && rotatedMouseY <= handleY + handleSize/2) { + return key; + } + } + return null; +}; + + nodeType.prototype.onCanvasMouseMove = function(e) { + if (this.textActionMode === 'moving' && this.activeTextObject) { + if (this.isTextDragging && this.activeTextObject) { + this.activeTextObject.x = e.offsetX + this.dragOffset.x; + this.activeTextObject.y = e.offsetY + this.dragOffset.y; + this.redrawPreviewCanvas(); + return; + } + } + if (this.interactionMode.startsWith("scaling_")) { + if (this.interactionMode === "scaling_tl" || this.interactionMode === "scaling_br") { + this.previewCanvas.style.cursor = "nwse-resize"; + } else { + this.previewCanvas.style.cursor = "nesw-resize"; + } + } else if (this.interactionMode === "moving") { + this.previewCanvas.style.cursor = "move"; + } else if (this.interactionMode === "rotating") { + this.previewCanvas.style.cursor = rotateCursorStyle; + } else { + if (this.movingLayer) { + const rect = this.previewCanvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + const handle = this.getHandleAtPos(e, mouseX, mouseY); + if (handle === "tl" || handle === "br") { this.previewCanvas.style.cursor = "nwse-resize"; } + else if (handle === "tr" || handle === "bl") { this.previewCanvas.style.cursor = "nesw-resize"; } + else if (handle === "rotate") { this.previewCanvas.style.cursor = rotateCursorStyle; } + else { this.previewCanvas.style.cursor = "move"; } + } else { + this.previewCanvas.style.cursor = "default"; + } + } + + if (this.interactionMode === "none") return; + + const props = this.layer_properties[this.movingLayer]; + if (!props) return; + const dx = e.clientX - this.dragStart.x; + const dy = e.clientY - this.dragStart.y; + if (this.interactionMode === "moving") { + if (!this.isDragging) return; + const baseImg = this.basePreviewImage; + if (!baseImg) return; + const scaleFactor = (baseImg.naturalWidth / this.movingLayerBounds.w) * props.scale; + props.offset_x = Math.round(this.initialOffsets.x + (dx * scaleFactor)); + props.offset_y = Math.round(this.initialOffsets.y + (dy * scaleFactor)); + } else if (this.interactionMode.startsWith("scaling_")) { + const angleInRadians = (props.rotation || 0) * Math.PI / 180; + const cos = Math.cos(-angleInRadians); + const sin = Math.sin(-angleInRadians); + const local_dx = dx * cos - dy * sin; + let newWidth; + + if (this.interactionMode === 'scaling_br' || this.interactionMode === 'scaling_tr') { + newWidth = this.initialBounds.w + local_dx; + } else if (this.interactionMode === 'scaling_bl' || this.interactionMode === 'scaling_tl') { + newWidth = this.initialBounds.w - local_dx; + } + + if (newWidth < 10) { newWidth = 10; } + + const newScale = this.initialScale * (newWidth / this.initialBounds.w); + props.scale = newScale; + } else if (this.interactionMode === "rotating") { + const mouseX = e.offsetX; + const mouseY = e.offsetY; + const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; + const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; + const currentMouseAngle = Math.atan2(mouseY - centerY, mouseX - centerX); + const angleDifference = currentMouseAngle - this.dragStartAngle; + const props = this.layer_properties[this.movingLayer]; + props.rotation = this.initialRotation + (angleDifference * 180 / Math.PI); + const rotationWidget = this.widgets.find(w => w.name === `rotation_${this.movingLayer}`); + if(rotationWidget) rotationWidget.value = props.rotation; + + this.redrawPreviewCanvas(); +} + const scaleWidget = this.widgets.find(w => w.name === `scale_${this.movingLayer}`); + if(scaleWidget) scaleWidget.value = props.scale; + const offsetXWidget = this.widgets.find(w => w.name === `offset_x_${this.movingLayer}`); + if(offsetXWidget) offsetXWidget.value = props.offset_x; + const offsetYWidget = this.widgets.find(w => w.name === `offset_y_${this.movingLayer}`); + if(offsetYWidget) offsetYWidget.value = props.offset_y; + const rotationWidget = this.widgets.find(w => w.name === `rotation_${this.movingLayer}`); + if(rotationWidget) rotationWidget.value = props.rotation; + + this.graph.setDirtyCanvas(true, true); + this.redrawPreviewCanvas(); + }; + nodeType.prototype.onCanvasMouseUp = function(e) { + if (this.isTextDragging) { + this.updatePropertiesJSON(); + this.textActionMode = 'none'; + this.activeTextObject = null; + this.isTextDragging = false; + this.redrawPreviewCanvas(); + } + if (this.interactionMode !== "none") { + this.isDragging = false; + this.interactionMode = "none"; + this.updatePropertiesJSON(); + } + }; + nodeType.prototype.onCanvasMouseLeave = function(e) { + if (this.previewCanvas) { + this.previewCanvas.style.cursor = "default"; + } + if (this.interactionMode !== "none") { + this.isDragging = false; + this.interactionMode = "none"; + this.updatePropertiesJSON(); + } + }; + +nodeType.prototype.initializeHeaderCanvases = function() { + for (let i = 1; i <= MAX_LAYERS; i++) { + const anchor = this.widgets.find(w => w.name === `header_anchor_${i}`); + if (!anchor || !anchor.inputEl || anchor.canvas) continue; + const canvas = document.createElement("canvas"); + anchor.canvas = canvas; + const container = anchor.inputEl.parentElement; + anchor.inputEl.style.display = "none"; + container.appendChild(canvas); + container.style.padding = "0px"; + container.style.margin = "0px"; + + canvas.addEventListener("mousedown", (e) => { + if (this.toolbar?.activeTool === 'mask_painter' && this.toolbar?.maskPainterManager) { + this.toolbar.maskPainterManager.hide(); + } + const layerName = e.currentTarget.dataset.layerName; + if (!layerName) { + return; + } + if (layerName === 'base_image') { + const bounds = this.base_image_properties?.replace_icon_bounds; + if (bounds && e.offsetX >= bounds.x && e.offsetX <= bounds.x + bounds.size && + e.offsetY >= bounds.y && e.offsetY <= bounds.y + bounds.size) { + this.handleBaseImageReplace(); + } + return; + } + const props = this.layer_properties[layerName]; + if (!props) return; + + const layer_index = parseInt(layerName.split("_")[1]); + const x = e.offsetX; + const y = e.offsetY; + if (props.trash_icon_bounds) { + const bounds = props.trash_icon_bounds; + if (x >= bounds.x && x <= bounds.x + bounds.size && y >= bounds.y && y <= bounds.y + bounds.size) { + if (confirm(`Are you sure you want to delete Layer ${layer_index}?`)) { + this.deleteLayer(layerName); + } + return; + } + } + + const widgetWidth = this.size[0] - 20; + const topPadding = 4; + const padding = 8; + const moveIconSize = 36; + const arrowSize = 24; + const lockSize = 36; + const thumbSize = 48; + const eyeSize = 36; + const allocatedHeight = 64; + const eyeX = widgetWidth - eyeSize - padding; + const thumbX = eyeX - thumbSize - padding; + const lockX = thumbX - lockSize - padding; + const arrowBlockX = lockX - arrowSize - padding; + const moveIconX = arrowBlockX - moveIconSize - (padding * 2); + const eyeY = topPadding + (thumbSize - eyeSize) / 2; + const lockY = topPadding + (thumbSize - lockSize) / 2; + const arrowUpY = topPadding + (thumbSize / 2 - arrowSize) + 4; + const arrowDownY = topPadding + (thumbSize / 2) - 4; + const moveIconY = topPadding + (thumbSize - moveIconSize) / 2; + const isInEye = x >= eyeX && x <= eyeX + eyeSize && y >= eyeY && y <= eyeY + eyeSize; + const isInLock = x >= lockX && x <= lockX + lockSize && y >= lockY && y <= lockY + lockSize; + const isInUpArrow = x >= arrowBlockX && x <= arrowBlockX + arrowSize && y >= arrowUpY && y <= arrowUpY + arrowSize; + const isInDownArrow = x >= arrowBlockX && x <= arrowBlockX + arrowSize && y >= arrowDownY && y <= arrowDownY + arrowSize; + const isInMoveIcon = x >= moveIconX && x <= moveIconX + moveIconSize && y >= moveIconY && y <= moveIconY + moveIconSize; + if (isInLock) { + this.accordionMode = !this.accordionMode; + this.refreshUI(); + } else if (isInEye) { + props.enabled = !props.enabled; + this.redrawPreviewCanvas(); + this.drawHeaderCanvas(canvas, layerName); + } else if (isInUpArrow) { + if (layer_index > 1) { this.moveLayer(layer_index, "up"); } + } else if (isInDownArrow) { + const total_layers = Object.keys(this.layer_properties).length; + if (layer_index < total_layers) { this.moveLayer(layer_index, "down"); } + } else if (isInMoveIcon && props.resize_mode === 'crop' && !(this.toolbar && (this.toolbar.activeTool || this.toolbar.selectionSubMenu?.style.display === 'flex'))) { + if (this.movingLayer === layerName) { + this.movingLayer = null; + } else { + this.movingLayer = layerName; + } + this.refreshUI(); + } else { + const isExpanding = props.layer_collapsed; + if (isExpanding) { + if (this.accordionMode) { + for (const otherLayerName in this.layer_properties) { + if (otherLayerName !== layerName) { + this.layer_properties[otherLayerName].layer_collapsed = true; + } + } + } + props.layer_collapsed = false; + } else { + let expanded_count = 0; + for (const key in this.layer_properties) { + if (!this.layer_properties[key].layer_collapsed) { + expanded_count++; + } + } + if (expanded_count > 1) { + props.layer_collapsed = true; + } + } + + if (this.toolbar.activeTool === 'brush') { + this.toolbar.brushManager.show(); + } + + this.refreshUI(); + } + this.updatePropertiesJSON(); + }); + } + }; +nodeType.prototype.drawHeaderCanvas = function(canvas, layerName) { + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + const widgetWidth = this.size[0] - 20; + const allocatedHeight = 64; + const ratio = window.devicePixelRatio || 1; + canvas.style.width = widgetWidth + "px"; + canvas.style.height = allocatedHeight + "px"; + canvas.width = widgetWidth * ratio; + canvas.height = allocatedHeight * ratio; + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.scale(ratio, ratio); + ctx.clearRect(0, 0, widgetWidth, allocatedHeight); + ctx.strokeStyle = "#555555"; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, widgetWidth - 2, allocatedHeight - 10); + const isBaseLayer = layerName === 'base_image'; + let layerImage = null; + if (this.loaded_preview_images) { + const imageKey = isBaseLayer ? 'base_image' : layerName; + layerImage = this.loaded_preview_images[imageKey]; + } + + const topPadding = 4; + const padding = 8; + const moveIconSize = 36; + const arrowSize = 24; + const lockSize = 36; + const thumbSize = 48; + const eyeSize = 36; + + const eyeX = widgetWidth - eyeSize - padding; + const thumbX = eyeX - thumbSize - padding; + const lockX = thumbX - lockSize - padding; + const arrowBlockX = lockX - arrowSize - padding; + const moveIconX = arrowBlockX - moveIconSize - (padding * 2); + const textY = topPadding + thumbSize / 2; + const thumbY = topPadding; + + if (isBaseLayer) { + canvas.dataset.layerName = 'base_image'; + ctx.fillStyle = '#FFFFFF'; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.font = "14px Arial"; + ctx.fillText(`▶ Base Image`, 5, textY); + const replaceIconSize = 36; + const replaceIconX = thumbX - replaceIconSize - padding; + const replaceIconY = topPadding + (thumbSize - replaceIconSize) / 2; + if (this.base_image_properties) { + this.base_image_properties.replace_icon_bounds = { x: replaceIconX, y: replaceIconY, size: replaceIconSize }; + } + ctx.save(); + ctx.translate(replaceIconX, replaceIconY); + ctx.scale(replaceIconSize / 24, replaceIconSize / 24); + ctx.strokeStyle = "#CCCCCC"; + ctx.lineWidth = 2; + ctx.stroke(replaceIconPath); + ctx.restore(); + ctx.fillStyle = "#353535"; + ctx.fillRect(thumbX, thumbY, thumbSize, thumbSize); + if (layerImage && layerImage.naturalWidth > 0) { + const imgRatio = layerImage.naturalWidth / layerImage.naturalHeight; + let destWidth, destHeight, destX, destY; + if (imgRatio > 1) { destWidth = thumbSize; destHeight = thumbSize / imgRatio; } + else { destHeight = thumbSize; destWidth = thumbSize * imgRatio; } + destX = thumbX + (thumbSize - destWidth) / 2; + destY = thumbY + (thumbSize - destHeight) / 2; + ctx.drawImage(layerImage, 0, 0, layerImage.naturalWidth, layerImage.naturalHeight, destX, destY, destWidth, destHeight); + } + + return; + } + + const props = this.layer_properties[layerName]; + if (!props) return; +canvas.dataset.layerName = layerName; + const layer_index = parseInt(layerName.split("_")[1]); + const total_layers = Object.keys(this.layer_properties).length; + + ctx.fillStyle = !props.layer_collapsed ? "#4CAF50" : LiteGraph.WIDGET_TEXT_COLOR; + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.font = "14px Arial"; + const triangle = props.layer_collapsed ? "▶" : "▼"; + const textMaxWidth = moveIconX - (padding * 3) - 24; + ctx.fillText(`${triangle} Layer ${layer_index}`, 5, textY, textMaxWidth); + const trashSize = 36; + const trashX = moveIconX - trashSize - padding; + const trashY = topPadding + (thumbSize - trashSize) / 2; + props.trash_icon_bounds = { x: trashX, y: trashY, size: trashSize }; + ctx.save(); + ctx.translate(trashX, trashY); + ctx.scale(trashSize / 24, trashSize / 24); + ctx.strokeStyle = "#F44336"; + ctx.lineWidth = 2; + ctx.stroke(trashIconPath); + ctx.restore(); + + const moveIconY = topPadding + (thumbSize - moveIconSize) / 2; + const isMoving = this.movingLayer === layerName; + const isMoveDisabled = props.resize_mode !== 'crop' || + (this.toolbar && (this.toolbar.activeTool || this.toolbar.selectionSubMenu?.style.display === 'flex')); + ctx.save(); + ctx.translate(moveIconX, moveIconY); + ctx.scale(moveIconSize / 24, moveIconSize / 24); + + ctx.strokeStyle = isMoving ? "#F44" : (isMoveDisabled ? "#555" : LiteGraph.WIDGET_TEXT_COLOR); + ctx.lineWidth = 2; + ctx.stroke(moveIconPath); + ctx.restore(); + + const isFirst = layer_index <= 1; + const isLast = layer_index >= total_layers; + const arrowUpY = topPadding + (thumbSize / 2 - arrowSize) + 4; + const arrowDownY = topPadding + (thumbSize / 2) - 4; + ctx.save(); + ctx.translate(arrowBlockX, arrowUpY); + ctx.strokeStyle = isFirst ? "#555" : LiteGraph.WIDGET_TEXT_COLOR; + ctx.lineWidth = 3; + ctx.stroke(arrowUpPath); + ctx.restore(); + ctx.save(); + ctx.translate(arrowBlockX, arrowDownY); + ctx.strokeStyle = isLast ? "#555" : LiteGraph.WIDGET_TEXT_COLOR; + ctx.lineWidth = 3; + ctx.stroke(arrowDownPath); + ctx.restore(); + + const lockY = topPadding + (thumbSize - lockSize) / 2; + ctx.save(); + ctx.translate(lockX, lockY); + ctx.scale(lockSize / 24, lockSize / 24); + ctx.strokeStyle = this.accordionMode ? "#F44" : "#6C6"; + ctx.lineWidth = 2; + ctx.stroke(this.accordionMode ? lockIconPath : unlockIconPath); + ctx.restore(); + + ctx.fillStyle = "#353535"; + ctx.fillRect(thumbX, thumbY, thumbSize, thumbSize); + if (layerImage && layerImage.naturalWidth > 0) { + const imgRatio = layerImage.naturalWidth / layerImage.naturalHeight; + let destWidth, destHeight, destX, destY; + if (imgRatio > 1) { destWidth = thumbSize; destHeight = thumbSize / imgRatio; } + else { destHeight = thumbSize; destWidth = thumbSize * imgRatio; } + destX = thumbX + (thumbSize - destWidth) / 2; + destY = thumbY + (thumbSize - destHeight) / 2; + ctx.drawImage(layerImage, 0, 0, layerImage.naturalWidth, layerImage.naturalHeight, destX, destY, destWidth, destHeight); + } + + const eyeY = topPadding + (thumbSize - eyeSize) / 2; + ctx.save(); + ctx.translate(eyeX, eyeY); + ctx.scale(eyeSize / 24, eyeSize / 24); + ctx.strokeStyle = props.enabled ? "#4CAF50" : "#F44"; + ctx.lineWidth = 1.5; + ctx.stroke(eyeIconPath); + if (!props.enabled) { ctx.stroke(eyeSlashPath); } + ctx.restore(); + }; + +nodeType.prototype.handleBaseImageReplace = function() { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/jpeg,image/png,image/webp'; + fileInput.style.display = 'none'; + fileInput.onchange = async (e) => { + if (!e.target.files.length) { + document.body.removeChild(fileInput); + return; + } + const originalFile = e.target.files[0]; + try { + const staticFilename = "layersystem_base.png"; + 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(); + + this.base_image_properties = { filename: data.name, details: data }; + this.updatePropertiesJSON(); + app.queuePrompt(); + } catch (error) { + console.error("[Layer System] Error replacing base image:", error); + } finally { + document.body.removeChild(fileInput); + } + }; + document.body.appendChild(fileInput); + fileInput.click(); +}; + + nodeType.prototype.updateLayerVisibility = function(layerName) { + const props = this.layer_properties[layerName]; + if (!props) return; + const isLayerCollapsed = props.layer_collapsed; + const isColorCollapsed = props.color_section_collapsed; + const showTransformForCrop = props.resize_mode === 'crop'; + const allWidgets = this.widgets.filter(w => + (w.name && typeof w.name === 'string' && w.name.endsWith(`_${layerName}`)) || + (w.value && typeof w.value === 'string' && w.value.endsWith(`_${layerName}`)) + ); + + for(const w of allWidgets) { + if (w.name.startsWith(`header_anchor_`)) continue; + let isHidden = isLayerCollapsed; + if (w.name.startsWith("spacer_for_")) { + isHidden = false; + } else if (w.name === `toggle_color_${layerName}`) { + } else if (["brightness", "contrast", "saturation", "color_r", "color_g", "color_b"].some(p => w.name.startsWith(p))) { + isHidden = isLayerCollapsed || isColorCollapsed; + } else if (["scale", "offset_x", "offset_y", "rotation"].some(p => w.name.startsWith(p))) { + isHidden = isLayerCollapsed || !showTransformForCrop; + } + + w.hidden = isHidden; + if (!w.originalComputeSize) w.originalComputeSize = w.computeSize; + w.computeSize = isHidden ? () => [0, -4] : w.originalComputeSize; + } + + resizeHeight.call(this); + }; + +nodeType.prototype.refreshUI = function() { + this.updateLayerWidgets(); + const activeLayerKeys = Object.keys(this.layer_properties); + const activeLayers = new Set(activeLayerKeys); + + const baseAnchor = this.widgets.find(w => w.name === `header_anchor_1`); + if (baseAnchor) { + if (this.base_image_properties) { + baseAnchor.hidden = false; + baseAnchor.computeSize = (width) => [width, 64]; + if (baseAnchor.canvas) { + this.drawHeaderCanvas(baseAnchor.canvas, 'base_image'); + } + } else { + baseAnchor.hidden = true; + baseAnchor.computeSize = () => [0, -4]; + } + } + + for (let i = 2; i <= MAX_LAYERS; i++) { + const layerName = `layer_${i - 1}`; + const anchor = this.widgets.find(w => w.name === `header_anchor_${i}`); + if (!anchor) continue; + if (activeLayers.has(layerName)) { + anchor.hidden = false; + anchor.computeSize = (width) => [width, 64]; + if (anchor.canvas) { + this.drawHeaderCanvas(anchor.canvas, layerName); + } + this.updateLayerVisibility(layerName); + } else { + anchor.hidden = true; + anchor.computeSize = () => [0, -4]; + } + } + + if (!this.base_image_properties && activeLayerKeys.length === 1) { + const layerName = activeLayerKeys[0]; + if (this.layer_properties[layerName]) { + this.layer_properties[layerName].layer_collapsed = false; + this.updateLayerVisibility(layerName); + } + } + + if (this.toolbar) { + if (this.toolbar.activeTool === 'mask') { + this.toolbar.maskManager.show(); + } else { + this.toolbar.maskManager.hide(); + } + } + + this.updatePropertiesJSON(); + resizeHeight.call(this); +}; + +nodeType.prototype.moveLayer = function(layer_index, direction) { + let layers_array = Object.entries(this.layer_properties).map(([name, props]) => { + const maskName = name.replace('layer_', 'mask_'); + return { + name, + props, + preview_img: this.loaded_preview_images ? this.loaded_preview_images[name] : null, + preview_data: this.preview_data ? this.preview_data[name] : null, + mask_img: this.loaded_preview_images ? this.loaded_preview_images[maskName] : null, + mask_data: this.preview_data ? this.preview_data[maskName] : null + }; + }); + + const fromIndex = layers_array.findIndex(l => l.name === `layer_${layer_index}`); + if (fromIndex === -1) return; + + const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1; + const element = layers_array.splice(fromIndex, 1)[0]; + layers_array.splice(toIndex, 0, element); + this.layer_properties = {}; + const old_loaded_preview_images = this.loaded_preview_images || {}; + const old_preview_data = this.preview_data || {}; + this.loaded_preview_images = { base_image: old_loaded_preview_images['base_image'] }; + this.preview_data = { base_image: old_preview_data['base_image'] }; + layers_array.forEach((layer, index) => { + const newLayerName = `layer_${index + 1}`; + const newMaskName = `mask_${index + 1}`; + this.layer_properties[newLayerName] = layer.props; + if (layer.preview_img) this.loaded_preview_images[newLayerName] = layer.preview_img; + if (layer.preview_data) this.preview_data[newLayerName] = layer.preview_data; + if (layer.mask_img) this.loaded_preview_images[newMaskName] = layer.mask_img; + if (layer.mask_data) this.preview_data[newMaskName] = layer.mask_data; + if (this.movingLayer === layer.name) { + this.movingLayer = newLayerName; + } + }); + this.updatePropertiesJSON(); + this.refreshUI(); + this.redrawPreviewCanvas(); +}; +nodeType.prototype.addLayerWidgets = function(layer_name) { + if (!this.layer_properties[layer_name]) { + this.layer_properties[layer_name] = { + blend_mode: "normal", opacity: 1.0, enabled: true, resize_mode: "crop", scale: 1.0, offset_x: 0, offset_y: 0, + 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: true, + internal_mask_filename: null, + internal_mask_details: null, + }; + } + const props = this.layer_properties[layer_name]; + const allWidgets = []; + const topSpacer = { name: `top_spacer_for_${layer_name}`, type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] }; + this.widgets.push(topSpacer); + allWidgets.push(topSpacer); + const redrawCallback = (prop, v) => { + props[prop] = v; + this.updatePropertiesJSON(); + this.redrawPreviewCanvas(); + this.graph.setDirtyCanvas(true, true); + }; + + allWidgets.push(this.addWidget("combo", `blend_mode_${layer_name}`, props.blend_mode, (v) => { props.blend_mode = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); }, {values: BLEND_MODES})); + allWidgets.push(this.addWidget("number", `opacity_${layer_name}`, props.opacity, (v) => { props.opacity = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); }, {min: 0.0, max: 1.0, step: 0.1, precision: 2})); + const colorToggle = this.addWidget("toggle", `toggle_color_${layer_name}`, !props.color_section_collapsed, (v) => { + props.color_section_collapsed = !v; this.refreshUI(); this.updatePropertiesJSON(); + }, { on: "▼ Color Adjust", off: "▶ Color Adjust" }); + allWidgets.push(colorToggle); + + allWidgets.push(this.addWidget("number", `brightness_${layer_name}`, props.brightness, (v) => redrawCallback('brightness', v), {min: -1.0, max: 1.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `contrast_${layer_name}`, props.contrast, (v) => redrawCallback('contrast', v), {min: -1.0, max: 1.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `saturation_${layer_name}`, props.saturation, (v) => redrawCallback('saturation', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `color_r_${layer_name}`, props.color_r, (v) => redrawCallback('color_r', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `color_g_${layer_name}`, props.color_g, (v) => redrawCallback('color_g', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `color_b_${layer_name}`, props.color_b, (v) => redrawCallback('color_b', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("toggle", `invert_mask_${layer_name}`, !!props.invert_mask, (v) => { + props.invert_mask = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); + })); + const resizeModeWidget = this.addWidget("combo", `resize_mode_${layer_name}`, props.resize_mode, (v) => { + props.resize_mode = v; if (v !== 'crop' && this.movingLayer === layer_name) { this.movingLayer = null; } + this.refreshUI(); + this.updatePropertiesJSON(); this.redrawPreviewCanvas(); + }, { values: RESIZE_MODES }); + allWidgets.push(resizeModeWidget); + allWidgets.push(this.addWidget("number", `scale_${layer_name}`, props.scale, (v) => redrawCallback('scale', v), {min: 0.01, max: 10.0, step: 0.1, precision: 2 })); + allWidgets.push(this.addWidget("number", `offset_x_${layer_name}`, props.offset_x, (v) => redrawCallback('offset_x', v), {min: -8192, max: 8192, step: 1 })); + allWidgets.push(this.addWidget("number", `offset_y_${layer_name}`, props.offset_y, (v) => redrawCallback('offset_y', v), {min: -8192, max: 8192, step: 1 })); + allWidgets.push(this.addWidget("number", `rotation_${layer_name}`, props.rotation, (v) => redrawCallback('rotation', v), {min: -360.0, max: 360.0, step: 1, precision: 1 })); + const bottomSpacer = { name: `bottom_spacer_for_${layer_name}`, type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] }; + this.widgets.push(bottomSpacer); + allWidgets.push(bottomSpacer); + for (const w of allWidgets) { + if (!w.originalComputeSize) w.originalComputeSize = w.computeSize; + } +}; + +nodeType.prototype.updateLayerWidgets = function() { + this.widgets = this.widgets.filter(w => + w.name.includes("_anchor") || + w.name === "_properties_json" || + w.name === "Add Image" || + w.name === "global_top_spacer" + ); + + const activeLayerKeys = Object.keys(this.layer_properties); + activeLayerKeys.sort((a, b) => parseInt(a.split("_")[1]) - parseInt(b.split("_")[1])); + + for (const layerName of activeLayerKeys) { + this.addLayerWidgets(layerName); + } +}; + +nodeType.prototype.getActiveLayer = function() { + if (!this.layer_properties) return null; + const openLayerName = Object.keys(this.layer_properties).find(name => this.layer_properties[name]?.layer_collapsed === false); + if (openLayerName) { + const layerIndex = parseInt(openLayerName.split("_")[1]); + const displayIndex = this.layer_order ? this.layer_order.indexOf(openLayerName) + 1 : layerIndex; + return { name: openLayerName, index: layerIndex, displayIndex: displayIndex }; + } + return null; +}; + +nodeType.prototype.handleDisconnectedInputs = function() { + const connected_layer_names = new Set(this.inputs.filter(i => i.name.startsWith("layer_") && i.link !== null).map(i => i.name)); + const inputs_to_remove = []; + const props_to_remove = []; + for (const key in this.layer_properties) { + if (!connected_layer_names.has(key)) { + props_to_remove.push(key); + const layer_input = this.inputs.find(i => i.name === key); + if(layer_input) inputs_to_remove.push(layer_input); + const mask_input = this.inputs.find(i => i.name === key.replace("layer_", "mask_")); + if(mask_input) inputs_to_remove.push(mask_input); + } + } + props_to_remove.forEach(key => delete this.layer_properties[key]); + inputs_to_remove.sort((a,b) => this.inputs.indexOf(b) - this.inputs.indexOf(a)).forEach(i => this.removeInput(this.inputs.indexOf(i))); + + const final_props = {}; + const final_images = {}; + const final_data = {}; + for (const key in this.loaded_preview_images) { + if (!key.startsWith("layer_") && !key.startsWith("mask_")) { + final_images[key] = this.loaded_preview_images[key]; + } + } + if (this.preview_data) { + for (const key in this.preview_data) { + if (!key.startsWith("layer_") && !key.startsWith("mask_")) { + final_data[key] = this.preview_data[key]; + } + } + } + + const remaining_layers = this.inputs.filter(i => i.name.startsWith("layer_")); + remaining_layers.sort((a, b) => this.inputs.indexOf(a) - this.inputs.indexOf(b)); + remaining_layers.forEach((input, i) => { + const old_name = input.name; + const new_name = `layer_${i + 1}`; + const old_mask_name = old_name.replace("layer_", "mask_"); + const new_mask_name = new_name.replace("layer_", "mask_"); + if (this.layer_properties[old_name]) { + final_props[new_name] = this.layer_properties[old_name]; + } + + if (this.loaded_preview_images[old_name]) { + final_images[new_name] = this.loaded_preview_images[old_name]; + } + if (this.loaded_preview_images[old_mask_name]) { + final_images[new_mask_name] = this.loaded_preview_images[old_mask_name]; + } + if (this.preview_data && this.preview_data[old_name]) { + final_data[new_name] = this.preview_data[old_name]; + } + if (this.preview_data && this.preview_data[old_mask_name]) { + final_data[new_mask_name] = this.preview_data[old_mask_name]; + } + input.name = new_name; + const mask_input = this.inputs.find(m => m.name === old_mask_name); + if (mask_input) { + mask_input.name = new_mask_name; + } + }); + this.layer_properties = final_props; + this.loaded_preview_images = final_images; + this.preview_data = final_data; + + const finalLayerNames = Object.keys(this.layer_properties); + if (finalLayerNames.length > 0) { + const isAnyLayerExpanded = finalLayerNames.some(name => + !this.layer_properties[name].layer_collapsed + ); + if (!isAnyLayerExpanded) { + this.layer_properties[finalLayerNames[0]].layer_collapsed = false; + } + } +}; + + nodeType.prototype.ensureWildcardInputs = function () { + const layerInputs = this.inputs.filter(i => i.name.startsWith("layer_")); + if(layerInputs.length >= MAX_LAYERS) return; + const lastLayerInput = layerInputs.sort((a,b) => parseInt(a.name.split("_")[1]) - parseInt(b.name.split("_")[1])).pop(); + if (!lastLayerInput || lastLayerInput.link !== null) { + const newIndex = lastLayerInput ? parseInt(lastLayerInput.name.split("_")[1]) + 1 : 1; + if (newIndex > MAX_LAYERS) return; + this.addInput(`layer_${newIndex}`, "IMAGE"); + this.addInput(`mask_${newIndex}`, "MASK"); + } + }; + +nodeType.prototype.handleInternalImageLoad = function() { + if (Object.keys(this.layer_properties).length >= MAX_LAYERS) { + alert(`Maximum number of layers (${MAX_LAYERS}) reached.`); + return; + } + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/jpeg,image/png,image/webp'; + fileInput.style.display = 'none'; + + fileInput.onchange = async (e) => { + if (!e.target.files.length) { + document.body.removeChild(fileInput); + return; + } + const originalFile = e.target.files[0]; + + try { + const isBaseImage = !this.base_image_properties; + 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 --- + + // 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é + + 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.basePreviewImage = newImage; + } else { + const existingIndices = new Set(Object.keys(this.layer_properties).map(k => parseInt(k.split('_')[1]))); + let newIndex = 1; + while (existingIndices.has(newIndex)) { newIndex++; } + const layerName = `layer_${newIndex}`; + + if (this.accordionMode) { + for (const key in this.layer_properties) { + this.layer_properties[key].layer_collapsed = true; + } + } + this.layer_properties[layerName] = { + source_filename: data.name, + source_details: data, + blend_mode: "normal", opacity: 1.0, enabled: true, resize_mode: "crop", scale: 1.0, offset_x: 0, offset_y: 0, + 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.updatePropertiesJSON(); + this.refreshUI(); + this.redrawPreviewCanvas(); // On redessine l'aperçu localement + + // On ne lance PAS de rendu backend + // app.queuePrompt(); + + } catch (error) { + console.error("Error uploading file:", error); + } finally { + document.body.removeChild(fileInput); + } + }; + document.body.appendChild(fileInput); + fileInput.click(); +}; + +nodeType.prototype.deleteLayer = function(layerNameToDelete) { + if (this.movingLayer === layerNameToDelete) { + this.movingLayer = null; + } + + const layerToDeleteProps = this.layer_properties[layerNameToDelete]; + if (layerToDeleteProps && layerToDeleteProps.source_details) { + const fileDetails = layerToDeleteProps.source_details; + const deleteFileOnServer = async (details) => { + try { + if (!details || !details.name) return; + await fetch("/layersystem/delete_file", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ filename: details.name, subfolder: details.subfolder }), + }); + } catch (e) { console.error("Failed to delete file:", e); } + }; + deleteFileOnServer(fileDetails); + if (layerToDeleteProps.internal_mask_details) deleteFileOnServer(layerToDeleteProps.internal_mask_details); + if (layerToDeleteProps.internal_preview_mask_details) deleteFileOnServer(layerToDeleteProps.internal_preview_mask_details); + } + + const layersToKeep = []; + const sortedKeys = Object.keys(this.layer_properties).sort((a, b) => parseInt(a.split('_')[1]) - parseInt(b.split('_')[1])); + + for (const name of sortedKeys) { + if (name !== layerNameToDelete) { + const maskName = name.replace('layer_', 'mask_'); + layersToKeep.push({ + props: JSON.parse(JSON.stringify(this.layer_properties[name])), + preview_img: this.loaded_preview_images[name], + preview_data: this.preview_data[name] ? JSON.parse(JSON.stringify(this.preview_data[name])) : undefined, + mask_img: this.loaded_preview_images[maskName], + mask_data: this.preview_data[maskName] ? JSON.parse(JSON.stringify(this.preview_data[maskName])) : undefined, + }); + } + } + + const new_layer_properties = {}; + const new_loaded_preview_images = { base_image: this.loaded_preview_images.base_image }; + const new_preview_data = { base_image: this.preview_data.base_image }; + + layersToKeep.forEach((layer, index) => { + const newLayerName = `layer_${index + 1}`; + const newMaskName = `mask_${index + 1}`; + new_layer_properties[newLayerName] = layer.props; + if (layer.preview_img) new_loaded_preview_images[newLayerName] = layer.preview_img; + if (layer.preview_data) new_preview_data[newLayerName] = layer.preview_data; + if (layer.mask_img) new_loaded_preview_images[newMaskName] = layer.mask_img; + if (layer.mask_data) new_preview_data[newMaskName] = layer.mask_data; + }); + + this.layer_properties = new_layer_properties; + this.loaded_preview_images = new_loaded_preview_images; + this.preview_data = new_preview_data; + + this.updatePropertiesJSON(); + this.refreshUI(); + this.redrawPreviewCanvas(); +}; + + nodeType.prototype.logFullState = function(label) { + console.log(`\n\n--- DEBUG STATE: ${label} ---`); + console.log("PROPRIÉTÉS DES CALQUES:", JSON.parse(JSON.stringify(this.layer_properties))); + console.log("DONNÉES DE PREVIEW:", this.preview_data); + console.log("--- FIN DEBUG STATE ---\n\n"); +}; + +nodeType.prototype.updatePropertiesJSON = function() { + const mainDataWidget = this.widgets.find(w => w.name === "_layer_system_data" || w.name === "_properties_json"); + if (mainDataWidget) { + const full_properties = { + base: this.base_image_properties, + layers: this.layer_properties, + texts: this.toolbar ? this.toolbar.getTexts() : [], + preview_width: this.previewCanvas ? this.previewCanvas.width : 512, + preview_height: this.previewCanvas ? this.previewCanvas.height : 512, + toolbar_width: this.toolbar ? this.toolbar.width : 0 + }; + mainDataWidget.value = JSON.stringify(full_properties); + } + this.widgets.forEach(widget => { + if (widget.name.includes("_anchor")) { + widget.value = null; + } + }); + }; + }, +}); \ No newline at end of file From 0a73f7537fa982d4be3a65d581761442b74a8ea9 Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 12:25:45 +0200 Subject: [PATCH 04/20] Delete layer_system_final.js --- layer_system_final.js | 1788 ----------------------------------------- 1 file changed, 1788 deletions(-) delete mode 100644 layer_system_final.js diff --git a/layer_system_final.js b/layer_system_final.js deleted file mode 100644 index f7519b0..0000000 --- a/layer_system_final.js +++ /dev/null @@ -1,1788 +0,0 @@ -import { app } from "/scripts/app.js"; -import { Toolbar } from './toolbar.js'; -function applyMask(layerImage, maskImage) { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d', { willReadFrequently: true }); - canvas.width = layerImage.width; - canvas.height = layerImage.height; - ctx.drawImage(maskImage, 0, 0, layerImage.width, layerImage.height); - const maskData = ctx.getImageData(0, 0, canvas.width, canvas.height); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(layerImage, 0, 0); - const layerData = ctx.getImageData(0, 0, canvas.width, canvas.height); - for (let i = 0; i < maskData.data.length; i += 4) { - const luminance = maskData.data[i]; - layerData.data[i + 3] = luminance; - } - ctx.putImageData(layerData, 0, 0); - return canvas; -} -const BLEND_MODES = ["normal", "multiply", "screen", "overlay", "soft-light", "hard-light", "difference", "color-dodge", "color-burn"]; -const RESIZE_MODES = ["stretch", "fit", "cover", "crop"]; -const MAX_LAYERS = 11; -const rotateCursorSVG = ``; -const rotateCursorDataUri = `data:image/svg+xml;base64,${btoa(rotateCursorSVG)}`; -const rotateCursorStyle = `url(${rotateCursorDataUri}) 12 12, auto`; -const eyeIconPath = new Path2D("M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zM12 9c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"); -const eyeSlashPath = new Path2D("M2 4.27l2.28 2.28L3.27 7.5C1.94 8.85 1 10.34 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l2.12 2.12L19.73 19 2 4.27zM12 17c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.01.19-.02.38-.02.57 0 1.66 1.34 3 3 3 .19 0 .38-.01.57-.02l1.57 1.57C13.5 16.82 12.77 17 12 17zm7.88-8.13C21.06 10.15 22 11.08 23 12c-1.73 4.39-6 7.5-11 7.5-.94 0-1.84-.13-2.69-.36l2.03 2.03c.85.22 1.74.33 2.66.33 5 0 9.27-3.11 11-7.5-.73-1.83-2.1-3.38-3.73-4.54l-1.39 1.39zM12 9c.99 0 1.89.28 2.67.77l-1.1-1.1C13.04 8.28 12.53 8 12 8c-2.76 0-5 2.24-5 5 0 .53.08 1.04.23 1.53l-1.1-1.1c-.49-.78-.73-1.68-.73-2.63 0-2.76 2.24-5 5-5z"); -const lockIconPath = new Path2D("M17 8v-1a5 5 0 00-10 0v1H5v12h14V8h-2zm-5 7a2 2 0 110-4 2 2 0 010 4zM9 7V6a3 3 0 116 0v1H9z"); -const unlockIconPath = new Path2D("M9 7V6a3 3 0 116 0v1h2V6a5 5 0 00-10 0v1H5v12h14V8H9zm3 9a2 2 0 110-4 2 2 0 010 4z"); -const arrowUpPath = new Path2D("M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"); -const arrowDownPath = new Path2D("M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"); -const moveIconPath = new Path2D("M12 2 L12 22 M2 12 L22 12 M12 2 L8 6 M12 2 L16 6 M12 22 L8 18 M12 22 L16 18 M2 12 L6 8 M2 12 L6 16 M22 12 L18 8 M22 12 L18 16"); -const trashIconPath = new Path2D("M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"); -const replaceIconPath = new Path2D("M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"); -app.registerExtension({ - name: "LayerSystem.DynamicLayers", - - beforeRegisterNodeDef(nodeType, nodeData) { - if (nodeData.name !== "LayerSystem") { - return; - } - const resizeHeight = function() { - if (!this.size) return; - const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); - if (anchorWidget && this.basePreviewImage) { - const aspectRatio = this.basePreviewImage.naturalHeight / this.basePreviewImage.naturalWidth; - const toolbarWidth = this.toolbar ? this.toolbar.width : 0; - const contentWidth = this.size[0] - 20 - toolbarWidth; - const requiredHeight = contentWidth * aspectRatio; - - anchorWidget.computeSize = () => [this.size[0], requiredHeight]; - } - const newSize = this.computeSize(); - this.size[1] = newSize[1]; - this.onResize?.(this.size); - this.graph.setDirtyCanvas(true, true); - if (anchorWidget?.computeSize) { - delete anchorWidget.computeSize; - } - }; - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function(message) { - onExecuted?.apply(this, arguments); - if (message?.layer_previews && message.layer_previews[0]) { - const previewData = message.layer_previews[0]; - this.preview_data = previewData; - const imagePromises = Object.entries(previewData).map(([name, previewInfo]) => { - const url = previewInfo.url; - return new Promise((resolve, reject) => { - const img = new Image(); - img.crossOrigin = "anonymous"; - if (url) { - img.src = url + `?t=${Date.now()}`; - img.onload = () => resolve({ name, img }); - img.onerror = (err) => reject(err); - } else { - reject(new Error(`[Layer System] Missing preview URL for ${name}`)); - } - }); - }); - Promise.all(imagePromises) - .then(loadedImages => { - this.loaded_preview_images = loadedImages.reduce((acc, {name, img}) => { - acc[name] = img; - return acc; - }, {}); - this.basePreviewImage = this.loaded_preview_images.base_image; - resizeHeight.call(this); - this.redrawPreviewCanvas(); - this.refreshUI(); - }) - .catch(e => console.error("[Layer System] At least one preview image could not be loaded.", e)); - } - }; - - const onDrawBackground = nodeType.prototype.onDrawBackground; - nodeType.prototype.onDrawBackground = function(ctx) { - 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') { - this.toolbar.maskManager.positionToolbar(); - } - if (this.toolbar.contextualToolbar?.style.display !== 'none') { - this.toolbar.updateContextualToolbarPosition(); - } - if (this.toolbar.magicWandManager?.contextualToolbar?.style.display !== 'none') { - this.toolbar.magicWandManager.positionToolbar(); - } - if (this.toolbar.selectionSubMenu?.style.display !== 'none') { - this.toolbar.positionSelectionSubMenu(); - } - } -}; - const onNodeCreated = nodeType.prototype.onNodeCreated; - nodeType.prototype.onNodeCreated = function () { - onNodeCreated?.apply(this, arguments); - this.base_image_properties = null; - this.layer_properties = this.layer_properties || {}; - - const onRemoved_original = this.onRemoved; - this.onRemoved = () => { - onRemoved_original?.apply(this, arguments); - if (this.toolbar?.contextualToolbar) { - this.toolbar.contextualToolbar.remove(); - } - if (this.toolbar?.maskManager?.contextualToolbar) { - this.toolbar.maskManager.contextualToolbar.remove(); - } - if (this.toolbar.selectionSubMenu) { - this.toolbar.selectionSubMenu.remove(); - } - if (this.toolbar.magicWandManager?.settingsToolbar) { - this.toolbar.magicWandManager.settingsToolbar.remove(); - } - if (this.toolbar?.brushManager?.toolbar) { - this.toolbar.brushManager.toolbar.remove(); - } - if (this.toolbar?.maskPainterManager?.toolbar) { - this.toolbar.maskPainterManager.toolbar.remove(); - } - }; - -const topSpacer = { - name: "global_top_spacer", - type: "CUSTOM_SPACER", - draw: () => {}, - computeSize: () => [0, 10] -}; -this.widgets.push(topSpacer); - this.addWidget( - "button", - "Add Image", - null, - () => { this.handleInternalImageLoad(); } - ); - - this.basePreviewImage = null; - this.preview_data = {}; - this.loaded_preview_images = {}; - this.redraw_req = null; - this.previewCanvasScale = 1.0; - this.movingLayer = null; - this.isDragging = false; - this.dragStart = { x: 0, y: 0 }; - this.initialOffsets = { x: 0, y: 0 }; - this.accordionMode = true; - this.interactionMode = "none"; - this.movingLayerBounds = null; - this.initialScale = 1.0; - this.initialBounds = null; - this.layerAspectRatio = 1.0; - this.initialRotation = 0.0; - this.textActionMode = "none"; - this.activeTextObject = null; - this.initialTextPos = { x: 0, y: 0 }; - this.isTextDragging = false; - this.dragOffset = { x: 0, y: 0 }; - this.needsFirstSync = true; - this._resizing = false; - this.rotationOffsetAngle = 0.0; - - this.toolbar = new Toolbar(this); - this.size[0] = 800; - - setTimeout(() => { - const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); - if (anchorWidget && anchorWidget.inputEl) { - const canvas = document.createElement("canvas"); - const container = anchorWidget.inputEl.parentElement; - container.style.padding = "0px"; - container.style.margin = "0px"; - anchorWidget.inputEl.style.display = "none"; - container.appendChild(canvas); - this.previewCanvas = canvas; - this.previewCtx = canvas.getContext("2d"); - - const overlayCanvas = document.createElement("canvas"); - overlayCanvas.id = "ls-overlay-canvas"; - overlayCanvas.style.position = "absolute"; - overlayCanvas.style.top = "0"; - overlayCanvas.style.left = "0"; - overlayCanvas.style.pointerEvents = "none"; - container.appendChild(overlayCanvas); - this.overlayCanvas = overlayCanvas; - - const anchorIndex = this.widgets.indexOf(anchorWidget); - if (anchorIndex > 0) { - this.widgets.splice(anchorIndex, 1); - this.widgets.unshift(anchorWidget); - } - this.redrawPreviewCanvas(); -this.previewCanvas.addEventListener('mousedown', (e) => { - const mouseX = e.offsetX; - const mouseY = e.offsetY; - - if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { - this.toolbar.handleClick(e, mouseX, mouseY); - return; - } - - if (this.textActionMode === 'moving' && this.activeTextObject) { - const targetText = this.findTextElementAtPos(mouseX, mouseY); - if (targetText && targetText.id === this.activeTextObject.id) { - this.dragOffset = { - x: targetText.x - mouseX, - y: targetText.y - mouseY - }; - this.isTextDragging = true; - } - e.preventDefault(); - e.stopPropagation(); - return; - } - - if (this.toolbar.activeTool === 'magic_wand' && this.toolbar.magicWandManager) { - this.toolbar.magicWandManager.handleCanvasClick(e); - return; - } - if (this.toolbar.activeTool) { - this.toolbar.handleCanvasClick(e); - return; - } - - if (!this.movingLayer) return; - const props = this.layer_properties[this.movingLayer]; - if (!props) return; - const handle = this.getHandleAtPos(e); - if (handle && handle !== 'rotate') { - this.interactionMode = "scaling_" + handle; - this.dragStart = { x: e.clientX, y: e.clientY }; - this.initialScale = props.scale; - this.initialOffsets = { x: props.offset_x, y: props.offset_y }; - this.initialBounds = { ...this.movingLayerBounds }; - this.layerAspectRatio = this.initialBounds.w / this.initialBounds.h; - } else if (handle === 'rotate') { - this.interactionMode = "rotating"; - const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; - const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; - this.dragStartAngle = Math.atan2(mouseY - centerY, mouseX - centerX); - this.initialRotation = props.rotation || 0; - } else { - this.interactionMode = "moving"; - this.isDragging = true; - this.dragStart = { x: e.clientX, y: e.clientY }; - this.initialOffsets = { x: props.offset_x, y: props.offset_y }; - } - e.preventDefault(); - e.stopPropagation(); -}); - this.previewCanvas.addEventListener('mousemove', this.onCanvasMouseMove.bind(this)); - this.previewCanvas.addEventListener('mouseup', this.onCanvasMouseUp.bind(this)); - this.previewCanvas.addEventListener('mouseleave', this.onCanvasMouseLeave.bind(this)); - } - this.initializeHeaderCanvases(); - - const p_widget = this.widgets.find(w => w.name === "_properties_json"); - if(p_widget) { - p_widget.hidden = true; - p_widget.computeSize = () => [0, -4]; - } - if (this.loadedConfig) { - this.loadStateFromConfig(this.loadedConfig); -} else { - this.refreshUI(); -} - }, 0); - }; - -nodeType.prototype.onResize = function(size) { - if (this._resizing) return; - this._resizing = true; - if (!this.widgets || !this.size || !this.basePreviewImage || this.basePreviewImage.naturalWidth <= 0) { - this._resizing = false; - return; - } - - const anchorWidget = this.widgets.find(w => w.name === "_preview_anchor"); - if (anchorWidget) { - const aspectRatio = this.basePreviewImage.naturalHeight / this.basePreviewImage.naturalWidth; - const toolbarWidth = this.toolbar ? this.toolbar.width : 0; - const contentWidth = size[0] - 20 - toolbarWidth; - const requiredHeight = contentWidth * aspectRatio; - anchorWidget.computeSize = () => [size[0], requiredHeight]; - } - - const newComputedSize = this.computeSize(); - this.size[1] = newComputedSize[1]; - if (anchorWidget?.computeSize) { - delete anchorWidget.computeSize; - } - - if (this.previewCanvas) { - if (this.redraw_req) cancelAnimationFrame(this.redraw_req); - this.redraw_req = requestAnimationFrame(() => { - this.redrawPreviewCanvas(); - this.redraw_req = null; - }); - } - - if (this.base_image_properties) { - const baseHeaderAnchor = this.widgets.find(w => w.name === 'header_anchor_1'); - if (baseHeaderAnchor && baseHeaderAnchor.canvas) { - this.drawHeaderCanvas(baseHeaderAnchor.canvas, 'base_image'); - } - } - - const activeLayerKeys = Object.keys(this.layer_properties); - for (const layerName of activeLayerKeys) { - const layerIndex = parseInt(layerName.split('_')[1]); - const headerAnchor = this.widgets.find(w => w.name === `header_anchor_${layerIndex + 1}`); - if (headerAnchor && headerAnchor.canvas) { - this.drawHeaderCanvas(headerAnchor.canvas, layerName); - } - } - this._resizing = false; -}; - -nodeType.prototype.onConfigure = function(info) { - const onConfigureOriginal = nodeType.prototype.__proto__.onConfigure; - onConfigureOriginal?.apply(this, arguments); - this.loadedConfig = info; -}; - -nodeType.prototype.loadStateFromConfig = function(info) { - if (this.stateLoaded) return; - if (info.widgets_values) { - const jsonWidgetIndex = this.widgets.findIndex(w => w.name === "_properties_json"); - if (jsonWidgetIndex > -1 && info.widgets_values[jsonWidgetIndex]) { - try { - 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); - this.refreshUI(); - - } catch (e) { - console.error("[Layer System] Error loading JSON state", e); - this.refreshUI(); - } - } - } else { - this.refreshUI(); - } -}; - -nodeType.prototype.refreshPreviewsOnly = async function() { - const properties_widget = this.widgets.find(w => w.name === "_properties_json"); - if (!properties_widget) return; - - try { - const response = await fetch("/layersystem/refresh_previews", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ properties_json: properties_widget.value }), - }); - - if (!response.ok) { - throw new Error(`Server error: ${response.status}`); - } - - 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 }); - } - } catch (e) { - console.error("[Layer System] Failed to refresh previews:", e); - } -}; - - const onConnectionsChange = nodeType.prototype.onConnectionsChange; - nodeType.prototype.onConnectionsChange = function (side, slot, is_connected, link_info, io_slot) { - onConnectionsChange?.apply(this, arguments); - if (side === 1) { setTimeout(() => this.refreshUI(), 0); } - }; - - nodeType.prototype.redrawPreviewCanvas = function() { - if (!this.previewCanvas || !this.size || !this.basePreviewImage || !this.basePreviewImage.naturalWidth) { - if(this.previewCtx) this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height); - return; - } - - const canvas = this.previewCanvas; - const ctx = this.previewCtx; - const baseImg = this.basePreviewImage; - const availableWidth = this.size[0] - 20; - const toolbarWidth = this.toolbar.width; - const imageAreaWidth = availableWidth - toolbarWidth; - const aspectRatio = baseImg.naturalHeight / baseImg.naturalWidth; - const requiredHeight = imageAreaWidth * aspectRatio; - if (canvas.width !== availableWidth) { - canvas.width = availableWidth; - } - const finalHeight = Math.round(requiredHeight); - if (canvas.height !== finalHeight) { - canvas.height = finalHeight; - } - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const destX = toolbarWidth; - const destY = 0; - const destWidth = imageAreaWidth; - const destHeight = finalHeight; - ctx.drawImage(baseImg, destX, destY, destWidth, destHeight); - this.previewCanvasScale = destWidth / baseImg.naturalWidth; - - 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_"); - const maskImage = this.loaded_preview_images[maskName]; - if (!props || !props.enabled || !layerImage || !layerImage.naturalWidth || !layerImage.naturalHeight) { - continue; - } - ctx.save(); - let imageToDraw = layerImage; - if (props.brightness !== 0.0 || props.contrast !== 0.0 || props.saturation !== 1.0 || props.color_r !== 1.0 || props.color_g !== 1.0 || props.color_b !== 1.0) { - try { - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = layerImage.naturalWidth; tempCanvas.height = layerImage.naturalHeight; - const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); - tempCtx.drawImage(layerImage, 0, 0); - const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); - const data = imageData.data; - const brightness = props.brightness, contrastFactor = 1.0 + props.contrast, saturation = props.saturation, color_r = props.color_r, color_g = props.color_g, color_b = props.color_b; - for (let i = 0; i < data.length; i += 4) { - let r = data[i] / 255.0, g = data[i + 1] / 255.0, b = data[i + 2] / 255.0; - if (brightness !== 0.0) { r += brightness; g += brightness; b += brightness; } - if (props.contrast !== 0.0) { r = (r - 0.5) * contrastFactor + 0.5; g = (g - 0.5) * contrastFactor + 0.5; b = (b - 0.5) * contrastFactor + 0.5; } - if (color_r !== 1.0 || color_g !== 1.0 || color_b !== 1.0) { r *= color_r; g *= color_g; b *= color_b; } - if (saturation !== 1.0) { const gray = r * 0.299 + g * 0.587 + b * 0.114; r = gray * (1.0 - saturation) + r * saturation; g = gray * (1.0 - saturation) + g * saturation; b = gray * (1.0 - saturation) + b * saturation; } - data[i] = Math.max(0, Math.min(255, r * 255)); data[i + 1] = Math.max(0, Math.min(255, g * 255)); data[i + 2] = Math.max(0, Math.min(255, b * 255)); - } - tempCtx.putImageData(imageData, 0, 0); - imageToDraw = tempCanvas; - } catch (e) { - console.error(`[LayerSystem] ERREUR lors du traitement du calque ${layerName}:`, e); - imageToDraw = layerImage; - } - } - let finalImageToDraw = imageToDraw; - if (maskImage && maskImage.naturalWidth > 0) { - let maskToApply = maskImage; - if (props.invert_mask) { - const invertedMaskCanvas = document.createElement('canvas'); - invertedMaskCanvas.width = maskImage.naturalWidth; - invertedMaskCanvas.height = maskImage.naturalHeight; - const invertedCtx = invertedMaskCanvas.getContext('2d'); - invertedCtx.filter = 'invert(1)'; - invertedCtx.drawImage(maskImage, 0, 0); - maskToApply = invertedMaskCanvas; - } - finalImageToDraw = applyMask(imageToDraw, maskToApply); - } - let final_sx = 0, final_sy = 0, final_sw = finalImageToDraw.width, final_sh = finalImageToDraw.height; - let final_dx = destX, final_dy = destY, final_dw = destWidth, final_dh = destHeight; - - if (props.resize_mode === 'crop') { - final_dw = final_sw * props.scale * this.previewCanvasScale; - final_dh = final_sh * props.scale * this.previewCanvasScale; - final_dx = destX + (props.offset_x * this.previewCanvasScale) - final_dw/2 + destWidth/2; - final_dy = destY + (props.offset_y * this.previewCanvasScale) - final_dh/2 + destHeight/2; - } else { - const layerRatio = final_sw / final_sh; - const destContainerRatio = destWidth / destHeight; - switch(props.resize_mode) { - case 'fit': - if (layerRatio > destContainerRatio) { final_dh = final_dw / layerRatio; final_dy += (destHeight - final_dh) / 2; } - else { final_dw = final_dh * layerRatio; final_dx += (destWidth - final_dw) / 2; } - break; - case 'cover': - if (layerRatio > destContainerRatio) { - final_sw = final_sh * destContainerRatio; - final_sx = (finalImageToDraw.width - final_sw) / 2; - } else { - final_sh = final_sw / destContainerRatio; - final_sy = (finalImageToDraw.height - final_sh) / 2; - } - break; - case 'stretch': default: break; - } - } - - ctx.globalAlpha = props.opacity; - ctx.globalCompositeOperation = props.blend_mode === 'normal' ? 'source-over' : props.blend_mode; - if (props.resize_mode === 'crop') { - const centerX = final_dx + final_dw / 2; - const centerY = final_dy + final_dh / 2; - - ctx.save(); - ctx.translate(centerX, centerY); - const angleInRadians = (props.rotation || 0) * Math.PI / 180; - ctx.rotate(angleInRadians); - ctx.drawImage(finalImageToDraw, final_sx, final_sy, final_sw, final_sh, -final_dw / 2, -final_dh / 2, final_dw, final_dh); - if (this.movingLayer === layerName) { - ctx.strokeStyle = "red"; - ctx.lineWidth = 2; - ctx.strokeRect(-final_dw / 2, -final_dh / 2, final_dw, final_dh); - - this.movingLayerBounds = { x: final_dx, y: final_dy, w: final_dw, h: final_dh, center_x: centerX, center_y: centerY }; - - const handleSize = 8; - ctx.fillStyle = "white"; - ctx.strokeStyle = "black"; - ctx.lineWidth = 1; - const corners = [ - {x: -final_dw/2, y: -final_dh/2}, {x: final_dw/2, y: -final_dh/2}, - {x: -final_dw/2, y: final_dh/2}, {x: final_dw/2, y: final_dh/2} - ]; - for(const corner of corners) { - ctx.fillRect(corner.x - handleSize/2, corner.y - handleSize/2, handleSize, handleSize); - ctx.strokeRect(corner.x - handleSize/2, corner.y - handleSize/2, handleSize, handleSize); - } - - const rotHandleOffset = 20; - const rotHandleY = -final_dh/2 - rotHandleOffset; - - ctx.beginPath(); - ctx.moveTo(0, -final_dh/2); - ctx.lineTo(0, rotHandleY); - ctx.strokeStyle = "red"; - ctx.lineWidth = 2; - ctx.stroke(); - ctx.beginPath(); - ctx.arc(0, rotHandleY, handleSize / 2, 0, 2 * Math.PI); - ctx.fillStyle = "white"; - ctx.fill(); - ctx.strokeStyle = "black"; - ctx.lineWidth = 1; - ctx.stroke(); - } - - ctx.restore(); - } else { - ctx.drawImage(finalImageToDraw, final_sx, final_sy, final_sw, final_sh, final_dx, final_dy, final_dw, final_dh); - } - - ctx.restore(); - } - - if (this.toolbar && this.toolbar.selectedTextObject) { - const el = this.toolbar.selectedTextObject; - const metrics = this.getTextPreviewMetrics(el); - if (metrics) { - ctx.save(); - ctx.strokeStyle = '#FFD700'; - ctx.lineWidth = 2; - ctx.setLineDash([6, 3]); - - const padding = 5; - ctx.strokeRect( - metrics.x - padding, - metrics.y - padding, - metrics.width + (padding * 2), - metrics.height + (padding * 2) - ); - - ctx.restore(); - } - } - - this.toolbar.drawTextElements(ctx); - this.toolbar.draw(ctx); - }; - -nodeType.prototype.getTextPreviewMetrics = function(textEl) { - if (!this.basePreviewImage || !this.previewCanvas || !this.toolbar) { return null; } - const baseImageWidth = this.basePreviewImage.naturalWidth; - const previewAreaWidth = this.previewCanvas.width - this.toolbar.width; - if (baseImageWidth <= 0) return null; - const inverseRatio = previewAreaWidth / baseImageWidth; - const previewCenterX = previewAreaWidth / 2; - const previewCenterY = this.previewCanvas.height / 2; - const preview_size = textEl.size * inverseRatio; - const preview_offset_x = textEl.offset_x * inverseRatio; - const preview_offset_y = textEl.offset_y * inverseRatio; - const preview_x = this.toolbar.width + previewCenterX + preview_offset_x; - const preview_y = previewCenterY + preview_offset_y; - const ctx = this.previewCtx; - ctx.save(); - ctx.font = `${preview_size}px ${textEl.fontFamily || 'Arial'}`; - const metrics = ctx.measureText(textEl.text); - ctx.restore(); - const textHeight = (metrics.fontBoundingBoxAscent || 0) + (metrics.fontBoundingBoxDescent || 0); - const finalHeight = textHeight > 0 ? textHeight : preview_size; - return { - x: preview_x, - y: preview_y, - size: preview_size, - width: metrics.width, - height: finalHeight -10 - }; -}; -nodeType.prototype.findTextElementAtPos = function(mouseX, mouseY) { - if (!this.toolbar || !this.toolbar.textElements.length || !this.previewCanvas || !this.basePreviewImage) { - return null; - } - const ctx = this.previewCtx; - const baseImageWidth = this.basePreviewImage.naturalWidth; - const previewAreaWidth = this.previewCanvas.width - this.toolbar.width; - - if (baseImageWidth <= 0) return null; - const inverseRatio = previewAreaWidth / baseImageWidth; - const previewCenterX = previewAreaWidth / 2; - const previewCenterY = this.previewCanvas.height / 2; - ctx.save(); - - for (let i = this.toolbar.textElements.length - 1; i >= 0; i--) { - const textEl = this.toolbar.textElements[i]; - const preview_offset_x = textEl.offset_x * inverseRatio; - const preview_offset_y = textEl.offset_y * inverseRatio; - const preview_size = textEl.size * inverseRatio; - const preview_x = this.toolbar.width + previewCenterX + preview_offset_x; - const preview_y = previewCenterY + preview_offset_y; - - ctx.font = `${preview_size}px ${textEl.fontFamily || 'Arial'}`; - const metrics = ctx.measureText(textEl.text); - const textWidth = metrics.width; - const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent || preview_size; - if (mouseX >= preview_x && mouseX <= preview_x + textWidth && - mouseY >= preview_y && mouseY <= preview_y + textHeight) - { - ctx.restore(); - return textEl; - } - } - ctx.restore(); - return null; -}; -nodeType.prototype.editTextElement = function(textElement) { - if (this.toolbar.activeTextarea) this.toolbar.activeTextarea.remove(); - const metrics = this.getTextPreviewMetrics(textElement); - if (!metrics) { - console.error("[Layer System] Unable to find text position for editing."); - return; - } - const canvasRect = this.previewCanvas.getBoundingClientRect(); - const finalLeft = canvasRect.left + metrics.x; - const finalTop = canvasRect.top + metrics.y; - const finalFontSize = metrics.size; - const textInput = document.createElement("div"); - this.toolbar.activeTextarea = textInput; - textInput.contentEditable = true; - textInput.innerText = textElement.text; - Object.assign(textInput.style, { - position: 'fixed', - left: `${finalLeft}px`, - top: `${finalTop}px`, - fontFamily: textElement.fontFamily, - fontSize: `${finalFontSize}px`, - color: 'white', - background: 'rgba(20,20,20,0.9)', - border: '1px solid #FFD700', - padding: '5px', - zIndex: '9999', - minWidth: `${metrics.width}px`, - whiteSpace: 'pre-wrap' - }); - document.body.appendChild(textInput); - textInput.focus(); - document.execCommand('selectAll', false, null); - textInput.addEventListener('keydown', (evt) => evt.stopPropagation()); - const onFinish = () => { - const newText = textInput.innerText; - - if (newText === "") { - const index = this.toolbar.textElements.findIndex(el => el.id === textElement.id); - if (index > -1) this.toolbar.textElements.splice(index, 1); - } else { - textElement.text = newText; - } - this.updatePropertiesJSON(); - this.redrawPreviewCanvas(); - - if (textInput.parentElement) textInput.parentElement.removeChild(textInput); - this.toolbar.activeTextarea = null; - this.toolbar.hideContextualToolbar(); - }; - textInput.addEventListener('blur', onFinish); - textInput.addEventListener('keydown', (evt) => { - evt.stopPropagation(); - if (evt.key === 'Enter' && !evt.shiftKey) { - evt.preventDefault(); - onFinish(); - } - if (evt.key === 'Escape') { - textInput.innerText = textElement.text; - onFinish(); - } - }); -}; -nodeType.prototype.showTextContextMenu = function(clientX, clientY, textElement) { - const existingMenu = document.getElementById("text-context-menu"); - if (existingMenu) existingMenu.remove(); - - this.activeTextObject = textElement; - const menu = document.createElement("div"); - menu.id = "text-context-menu"; - Object.assign(menu.style, { - position: 'fixed', left: `${clientX}px`, top: `${clientY}px`, backgroundColor: '#333', - border: '1px solid #555', borderRadius: '4px', padding: '5px', zIndex: '10000', - display: 'flex', flexDirection: 'column', - }); - const actionNames = ['Modifier le texte', 'Déplacer', 'Redimensionner', 'Pivoter', 'Supprimer']; - - actionNames.forEach(actionName => { - const button = document.createElement("button"); - button.innerText = actionName; - Object.assign(button.style, { - backgroundColor: '#444', color: 'white', border: '1px solid #666', - padding: '8px 12px', margin: '2px', textAlign: 'left', cursor: 'pointer' - }); - button.onmouseover = () => button.style.backgroundColor = '#555'; - button.onmouseout = () => button.style.backgroundColor = '#444'; - - button.onclick = (e) => { - menu.remove(); - const currentTextElement = this.toolbar.textElements.find(el => el.id === textElement.id); - if (!currentTextElement) { - console.log("L'élément texte a été supprimé, action annulée."); - return; - } - switch (actionName) { - case 'Modifier le texte': - this.editTextElement(currentTextElement); - break; - case 'Déplacer': - this.textActionMode = 'moving'; - this.activeTextObject = currentTextElement; - this.redrawPreviewCanvas(); - break; - case 'Supprimer': - const index = this.toolbar.textElements.findIndex(el => el.id === currentTextElement.id); - if (index > -1) { - this.toolbar.textElements.splice(index, 1); - this.updatePropertiesJSON(); - this.redrawPreviewCanvas(); - } - break; - } - }; - menu.appendChild(button); - }); - document.body.appendChild(menu); - const closeMenuHandler = (e) => { - if (!menu.contains(e.target)) { - menu.remove(); - document.removeEventListener('mousedown', closeMenuHandler, true); - } - }; - - setTimeout(() => document.addEventListener('mousedown', closeMenuHandler, true), 0); -}; -nodeType.prototype.onCanvasMouseDown = function(e) { - - - const mouseX = e.offsetX; - const mouseY = e.offsetY; - if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { - this.toolbar.handleClick(e, mouseX, mouseY); - return; - } - - - if (this.toolbar.activeTool) { - this.toolbar.handleCanvasClick(e); - return; - } - if (!this.movingLayer) { - return; - } - const props = this.layer_properties[this.movingLayer]; - if (!props) return; - const handle = this.getHandleAtPos(e); - if (handle && handle !== 'rotate') { - this.interactionMode = "scaling_" + handle; - this.dragStart = { x: e.clientX, y: e.clientY }; - this.initialScale = props.scale; - this.initialOffsets = { x: props.offset_x, y: props.offset_y }; - this.initialBounds = { ...this.movingLayerBounds }; - this.layerAspectRatio = this.initialBounds.w / this.initialBounds.h; -} else if (handle === 'rotate') { - this.interactionMode = "rotating"; - const props = this.layer_properties[this.movingLayer]; - const mouseX = e.offsetX; - const mouseY = e.offsetY; - const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; - const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; - this.dragStartAngle = Math.atan2(mouseY - centerY, mouseX - centerX); - this.initialRotation = props.rotation || 0; -} else { - this.interactionMode = "moving"; - this.isDragging = true; - this.dragStart = { x: e.clientX, y: e.clientY }; - this.initialOffsets = { x: props.offset_x, y: props.offset_y }; - } - - e.preventDefault(); - e.stopPropagation(); -}; -nodeType.prototype.getHandleAtPos = function(e) { - if (!this.movingLayer || !this.movingLayerBounds || !this.previewCanvas) return null; - - const mouseX = e.offsetX; - const mouseY = e.offsetY; - const handleSize = 12; - const bounds = this.movingLayerBounds; - - const props = this.layer_properties[this.movingLayer]; - const angleInRadians = (props.rotation || 0) * Math.PI / 180; - const centerX = bounds.x + bounds.w / 2; - const centerY = bounds.y + bounds.h / 2; - const cos = Math.cos(-angleInRadians); - const sin = Math.sin(-angleInRadians); - const dx = mouseX - centerX; - const dy = mouseY - centerY; - const rotatedMouseX = dx * cos - dy * sin + centerX; - const rotatedMouseY = dx * sin + dy * cos + centerY; - - const rotHandleSize = 16; - const rotHandleX_unrotated = bounds.x + bounds.w / 2; - const rotHandleY_unrotated = bounds.y - 20; - - if (rotatedMouseX >= rotHandleX_unrotated - rotHandleSize / 2 && rotatedMouseX <= rotHandleX_unrotated + rotHandleSize / 2 && - rotatedMouseY >= rotHandleY_unrotated - rotHandleSize / 2 && rotatedMouseY <= rotHandleY_unrotated + rotHandleSize / 2) { - return "rotate"; - } - const corners = { - tl: { x: bounds.x, y: bounds.y }, - tr: { x: bounds.x + bounds.w, y: bounds.y }, - bl: { x: bounds.x, y: bounds.y + bounds.h }, - br: { x: bounds.x + bounds.w, y: bounds.y + bounds.h } - }; - for (const key in corners) { - const corner = corners[key]; - const handleX = corner.x; - const handleY = corner.y; - if (rotatedMouseX >= handleX - handleSize/2 && rotatedMouseX <= handleX + handleSize/2 && - rotatedMouseY >= handleY - handleSize/2 && rotatedMouseY <= handleY + handleSize/2) { - return key; - } - } - return null; -}; - - nodeType.prototype.onCanvasMouseMove = function(e) { - if (this.textActionMode === 'moving' && this.activeTextObject) { - if (this.isTextDragging && this.activeTextObject) { - this.activeTextObject.x = e.offsetX + this.dragOffset.x; - this.activeTextObject.y = e.offsetY + this.dragOffset.y; - this.redrawPreviewCanvas(); - return; - } - } - if (this.interactionMode.startsWith("scaling_")) { - if (this.interactionMode === "scaling_tl" || this.interactionMode === "scaling_br") { - this.previewCanvas.style.cursor = "nwse-resize"; - } else { - this.previewCanvas.style.cursor = "nesw-resize"; - } - } else if (this.interactionMode === "moving") { - this.previewCanvas.style.cursor = "move"; - } else if (this.interactionMode === "rotating") { - this.previewCanvas.style.cursor = rotateCursorStyle; - } else { - if (this.movingLayer) { - const rect = this.previewCanvas.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - const handle = this.getHandleAtPos(e, mouseX, mouseY); - if (handle === "tl" || handle === "br") { this.previewCanvas.style.cursor = "nwse-resize"; } - else if (handle === "tr" || handle === "bl") { this.previewCanvas.style.cursor = "nesw-resize"; } - else if (handle === "rotate") { this.previewCanvas.style.cursor = rotateCursorStyle; } - else { this.previewCanvas.style.cursor = "move"; } - } else { - this.previewCanvas.style.cursor = "default"; - } - } - - if (this.interactionMode === "none") return; - - const props = this.layer_properties[this.movingLayer]; - if (!props) return; - const dx = e.clientX - this.dragStart.x; - const dy = e.clientY - this.dragStart.y; - if (this.interactionMode === "moving") { - if (!this.isDragging) return; - const baseImg = this.basePreviewImage; - if (!baseImg) return; - const scaleFactor = (baseImg.naturalWidth / this.movingLayerBounds.w) * props.scale; - props.offset_x = Math.round(this.initialOffsets.x + (dx * scaleFactor)); - props.offset_y = Math.round(this.initialOffsets.y + (dy * scaleFactor)); - } else if (this.interactionMode.startsWith("scaling_")) { - const angleInRadians = (props.rotation || 0) * Math.PI / 180; - const cos = Math.cos(-angleInRadians); - const sin = Math.sin(-angleInRadians); - const local_dx = dx * cos - dy * sin; - let newWidth; - - if (this.interactionMode === 'scaling_br' || this.interactionMode === 'scaling_tr') { - newWidth = this.initialBounds.w + local_dx; - } else if (this.interactionMode === 'scaling_bl' || this.interactionMode === 'scaling_tl') { - newWidth = this.initialBounds.w - local_dx; - } - - if (newWidth < 10) { newWidth = 10; } - - const newScale = this.initialScale * (newWidth / this.initialBounds.w); - props.scale = newScale; - } else if (this.interactionMode === "rotating") { - const mouseX = e.offsetX; - const mouseY = e.offsetY; - const centerX = this.movingLayerBounds.x + this.movingLayerBounds.w / 2; - const centerY = this.movingLayerBounds.y + this.movingLayerBounds.h / 2; - const currentMouseAngle = Math.atan2(mouseY - centerY, mouseX - centerX); - const angleDifference = currentMouseAngle - this.dragStartAngle; - const props = this.layer_properties[this.movingLayer]; - props.rotation = this.initialRotation + (angleDifference * 180 / Math.PI); - const rotationWidget = this.widgets.find(w => w.name === `rotation_${this.movingLayer}`); - if(rotationWidget) rotationWidget.value = props.rotation; - - this.redrawPreviewCanvas(); -} - const scaleWidget = this.widgets.find(w => w.name === `scale_${this.movingLayer}`); - if(scaleWidget) scaleWidget.value = props.scale; - const offsetXWidget = this.widgets.find(w => w.name === `offset_x_${this.movingLayer}`); - if(offsetXWidget) offsetXWidget.value = props.offset_x; - const offsetYWidget = this.widgets.find(w => w.name === `offset_y_${this.movingLayer}`); - if(offsetYWidget) offsetYWidget.value = props.offset_y; - const rotationWidget = this.widgets.find(w => w.name === `rotation_${this.movingLayer}`); - if(rotationWidget) rotationWidget.value = props.rotation; - - this.graph.setDirtyCanvas(true, true); - this.redrawPreviewCanvas(); - }; - nodeType.prototype.onCanvasMouseUp = function(e) { - if (this.isTextDragging) { - this.updatePropertiesJSON(); - this.textActionMode = 'none'; - this.activeTextObject = null; - this.isTextDragging = false; - this.redrawPreviewCanvas(); - } - if (this.interactionMode !== "none") { - this.isDragging = false; - this.interactionMode = "none"; - this.updatePropertiesJSON(); - } - }; - nodeType.prototype.onCanvasMouseLeave = function(e) { - if (this.previewCanvas) { - this.previewCanvas.style.cursor = "default"; - } - if (this.interactionMode !== "none") { - this.isDragging = false; - this.interactionMode = "none"; - this.updatePropertiesJSON(); - } - }; - -nodeType.prototype.initializeHeaderCanvases = function() { - for (let i = 1; i <= MAX_LAYERS; i++) { - const anchor = this.widgets.find(w => w.name === `header_anchor_${i}`); - if (!anchor || !anchor.inputEl || anchor.canvas) continue; - const canvas = document.createElement("canvas"); - anchor.canvas = canvas; - const container = anchor.inputEl.parentElement; - anchor.inputEl.style.display = "none"; - container.appendChild(canvas); - container.style.padding = "0px"; - container.style.margin = "0px"; - - canvas.addEventListener("mousedown", (e) => { - if (this.toolbar?.activeTool === 'mask_painter' && this.toolbar?.maskPainterManager) { - this.toolbar.maskPainterManager.hide(); - } - const layerName = e.currentTarget.dataset.layerName; - if (!layerName) { - return; - } - if (layerName === 'base_image') { - const bounds = this.base_image_properties?.replace_icon_bounds; - if (bounds && e.offsetX >= bounds.x && e.offsetX <= bounds.x + bounds.size && - e.offsetY >= bounds.y && e.offsetY <= bounds.y + bounds.size) { - this.handleBaseImageReplace(); - } - return; - } - const props = this.layer_properties[layerName]; - if (!props) return; - - const layer_index = parseInt(layerName.split("_")[1]); - const x = e.offsetX; - const y = e.offsetY; - if (props.trash_icon_bounds) { - const bounds = props.trash_icon_bounds; - if (x >= bounds.x && x <= bounds.x + bounds.size && y >= bounds.y && y <= bounds.y + bounds.size) { - if (confirm(`Are you sure you want to delete Layer ${layer_index}?`)) { - this.deleteLayer(layerName); - } - return; - } - } - - const widgetWidth = this.size[0] - 20; - const topPadding = 4; - const padding = 8; - const moveIconSize = 36; - const arrowSize = 24; - const lockSize = 36; - const thumbSize = 48; - const eyeSize = 36; - const allocatedHeight = 64; - const eyeX = widgetWidth - eyeSize - padding; - const thumbX = eyeX - thumbSize - padding; - const lockX = thumbX - lockSize - padding; - const arrowBlockX = lockX - arrowSize - padding; - const moveIconX = arrowBlockX - moveIconSize - (padding * 2); - const eyeY = topPadding + (thumbSize - eyeSize) / 2; - const lockY = topPadding + (thumbSize - lockSize) / 2; - const arrowUpY = topPadding + (thumbSize / 2 - arrowSize) + 4; - const arrowDownY = topPadding + (thumbSize / 2) - 4; - const moveIconY = topPadding + (thumbSize - moveIconSize) / 2; - const isInEye = x >= eyeX && x <= eyeX + eyeSize && y >= eyeY && y <= eyeY + eyeSize; - const isInLock = x >= lockX && x <= lockX + lockSize && y >= lockY && y <= lockY + lockSize; - const isInUpArrow = x >= arrowBlockX && x <= arrowBlockX + arrowSize && y >= arrowUpY && y <= arrowUpY + arrowSize; - const isInDownArrow = x >= arrowBlockX && x <= arrowBlockX + arrowSize && y >= arrowDownY && y <= arrowDownY + arrowSize; - const isInMoveIcon = x >= moveIconX && x <= moveIconX + moveIconSize && y >= moveIconY && y <= moveIconY + moveIconSize; - if (isInLock) { - this.accordionMode = !this.accordionMode; - this.refreshUI(); - } else if (isInEye) { - props.enabled = !props.enabled; - this.redrawPreviewCanvas(); - this.drawHeaderCanvas(canvas, layerName); - } else if (isInUpArrow) { - if (layer_index > 1) { this.moveLayer(layer_index, "up"); } - } else if (isInDownArrow) { - const total_layers = Object.keys(this.layer_properties).length; - if (layer_index < total_layers) { this.moveLayer(layer_index, "down"); } - } else if (isInMoveIcon && props.resize_mode === 'crop' && !(this.toolbar && (this.toolbar.activeTool || this.toolbar.selectionSubMenu?.style.display === 'flex'))) { - if (this.movingLayer === layerName) { - this.movingLayer = null; - } else { - this.movingLayer = layerName; - } - this.refreshUI(); - } else { - const isExpanding = props.layer_collapsed; - if (isExpanding) { - if (this.accordionMode) { - for (const otherLayerName in this.layer_properties) { - if (otherLayerName !== layerName) { - this.layer_properties[otherLayerName].layer_collapsed = true; - } - } - } - props.layer_collapsed = false; - } else { - let expanded_count = 0; - for (const key in this.layer_properties) { - if (!this.layer_properties[key].layer_collapsed) { - expanded_count++; - } - } - if (expanded_count > 1) { - props.layer_collapsed = true; - } - } - - if (this.toolbar.activeTool === 'brush') { - this.toolbar.brushManager.show(); - } - - this.refreshUI(); - } - this.updatePropertiesJSON(); - }); - } - }; -nodeType.prototype.drawHeaderCanvas = function(canvas, layerName) { - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - const widgetWidth = this.size[0] - 20; - const allocatedHeight = 64; - const ratio = window.devicePixelRatio || 1; - canvas.style.width = widgetWidth + "px"; - canvas.style.height = allocatedHeight + "px"; - canvas.width = widgetWidth * ratio; - canvas.height = allocatedHeight * ratio; - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.scale(ratio, ratio); - ctx.clearRect(0, 0, widgetWidth, allocatedHeight); - ctx.strokeStyle = "#555555"; - ctx.lineWidth = 2; - ctx.strokeRect(1, 1, widgetWidth - 2, allocatedHeight - 10); - const isBaseLayer = layerName === 'base_image'; - let layerImage = null; - if (this.loaded_preview_images) { - const imageKey = isBaseLayer ? 'base_image' : layerName; - layerImage = this.loaded_preview_images[imageKey]; - } - - const topPadding = 4; - const padding = 8; - const moveIconSize = 36; - const arrowSize = 24; - const lockSize = 36; - const thumbSize = 48; - const eyeSize = 36; - - const eyeX = widgetWidth - eyeSize - padding; - const thumbX = eyeX - thumbSize - padding; - const lockX = thumbX - lockSize - padding; - const arrowBlockX = lockX - arrowSize - padding; - const moveIconX = arrowBlockX - moveIconSize - (padding * 2); - const textY = topPadding + thumbSize / 2; - const thumbY = topPadding; - - if (isBaseLayer) { - canvas.dataset.layerName = 'base_image'; - ctx.fillStyle = '#FFFFFF'; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.font = "14px Arial"; - ctx.fillText(`▶ Base Image`, 5, textY); - const replaceIconSize = 36; - const replaceIconX = thumbX - replaceIconSize - padding; - const replaceIconY = topPadding + (thumbSize - replaceIconSize) / 2; - if (this.base_image_properties) { - this.base_image_properties.replace_icon_bounds = { x: replaceIconX, y: replaceIconY, size: replaceIconSize }; - } - ctx.save(); - ctx.translate(replaceIconX, replaceIconY); - ctx.scale(replaceIconSize / 24, replaceIconSize / 24); - ctx.strokeStyle = "#CCCCCC"; - ctx.lineWidth = 2; - ctx.stroke(replaceIconPath); - ctx.restore(); - ctx.fillStyle = "#353535"; - ctx.fillRect(thumbX, thumbY, thumbSize, thumbSize); - if (layerImage && layerImage.naturalWidth > 0) { - const imgRatio = layerImage.naturalWidth / layerImage.naturalHeight; - let destWidth, destHeight, destX, destY; - if (imgRatio > 1) { destWidth = thumbSize; destHeight = thumbSize / imgRatio; } - else { destHeight = thumbSize; destWidth = thumbSize * imgRatio; } - destX = thumbX + (thumbSize - destWidth) / 2; - destY = thumbY + (thumbSize - destHeight) / 2; - ctx.drawImage(layerImage, 0, 0, layerImage.naturalWidth, layerImage.naturalHeight, destX, destY, destWidth, destHeight); - } - - return; - } - - const props = this.layer_properties[layerName]; - if (!props) return; -canvas.dataset.layerName = layerName; - const layer_index = parseInt(layerName.split("_")[1]); - const total_layers = Object.keys(this.layer_properties).length; - - ctx.fillStyle = !props.layer_collapsed ? "#4CAF50" : LiteGraph.WIDGET_TEXT_COLOR; - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.font = "14px Arial"; - const triangle = props.layer_collapsed ? "▶" : "▼"; - const textMaxWidth = moveIconX - (padding * 3) - 24; - ctx.fillText(`${triangle} Layer ${layer_index}`, 5, textY, textMaxWidth); - const trashSize = 36; - const trashX = moveIconX - trashSize - padding; - const trashY = topPadding + (thumbSize - trashSize) / 2; - props.trash_icon_bounds = { x: trashX, y: trashY, size: trashSize }; - ctx.save(); - ctx.translate(trashX, trashY); - ctx.scale(trashSize / 24, trashSize / 24); - ctx.strokeStyle = "#F44336"; - ctx.lineWidth = 2; - ctx.stroke(trashIconPath); - ctx.restore(); - - const moveIconY = topPadding + (thumbSize - moveIconSize) / 2; - const isMoving = this.movingLayer === layerName; - const isMoveDisabled = props.resize_mode !== 'crop' || - (this.toolbar && (this.toolbar.activeTool || this.toolbar.selectionSubMenu?.style.display === 'flex')); - ctx.save(); - ctx.translate(moveIconX, moveIconY); - ctx.scale(moveIconSize / 24, moveIconSize / 24); - - ctx.strokeStyle = isMoving ? "#F44" : (isMoveDisabled ? "#555" : LiteGraph.WIDGET_TEXT_COLOR); - ctx.lineWidth = 2; - ctx.stroke(moveIconPath); - ctx.restore(); - - const isFirst = layer_index <= 1; - const isLast = layer_index >= total_layers; - const arrowUpY = topPadding + (thumbSize / 2 - arrowSize) + 4; - const arrowDownY = topPadding + (thumbSize / 2) - 4; - ctx.save(); - ctx.translate(arrowBlockX, arrowUpY); - ctx.strokeStyle = isFirst ? "#555" : LiteGraph.WIDGET_TEXT_COLOR; - ctx.lineWidth = 3; - ctx.stroke(arrowUpPath); - ctx.restore(); - ctx.save(); - ctx.translate(arrowBlockX, arrowDownY); - ctx.strokeStyle = isLast ? "#555" : LiteGraph.WIDGET_TEXT_COLOR; - ctx.lineWidth = 3; - ctx.stroke(arrowDownPath); - ctx.restore(); - - const lockY = topPadding + (thumbSize - lockSize) / 2; - ctx.save(); - ctx.translate(lockX, lockY); - ctx.scale(lockSize / 24, lockSize / 24); - ctx.strokeStyle = this.accordionMode ? "#F44" : "#6C6"; - ctx.lineWidth = 2; - ctx.stroke(this.accordionMode ? lockIconPath : unlockIconPath); - ctx.restore(); - - ctx.fillStyle = "#353535"; - ctx.fillRect(thumbX, thumbY, thumbSize, thumbSize); - if (layerImage && layerImage.naturalWidth > 0) { - const imgRatio = layerImage.naturalWidth / layerImage.naturalHeight; - let destWidth, destHeight, destX, destY; - if (imgRatio > 1) { destWidth = thumbSize; destHeight = thumbSize / imgRatio; } - else { destHeight = thumbSize; destWidth = thumbSize * imgRatio; } - destX = thumbX + (thumbSize - destWidth) / 2; - destY = thumbY + (thumbSize - destHeight) / 2; - ctx.drawImage(layerImage, 0, 0, layerImage.naturalWidth, layerImage.naturalHeight, destX, destY, destWidth, destHeight); - } - - const eyeY = topPadding + (thumbSize - eyeSize) / 2; - ctx.save(); - ctx.translate(eyeX, eyeY); - ctx.scale(eyeSize / 24, eyeSize / 24); - ctx.strokeStyle = props.enabled ? "#4CAF50" : "#F44"; - ctx.lineWidth = 1.5; - ctx.stroke(eyeIconPath); - if (!props.enabled) { ctx.stroke(eyeSlashPath); } - ctx.restore(); - }; - -nodeType.prototype.handleBaseImageReplace = function() { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/jpeg,image/png,image/webp'; - fileInput.style.display = 'none'; - fileInput.onchange = async (e) => { - if (!e.target.files.length) { - document.body.removeChild(fileInput); - return; - } - const originalFile = e.target.files[0]; - try { - const staticFilename = "layersystem_base.png"; - 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(); - - this.base_image_properties = { filename: data.name, details: data }; - this.updatePropertiesJSON(); - app.queuePrompt(); - } catch (error) { - console.error("[Layer System] Error replacing base image:", error); - } finally { - document.body.removeChild(fileInput); - } - }; - document.body.appendChild(fileInput); - fileInput.click(); -}; - - nodeType.prototype.updateLayerVisibility = function(layerName) { - const props = this.layer_properties[layerName]; - if (!props) return; - const isLayerCollapsed = props.layer_collapsed; - const isColorCollapsed = props.color_section_collapsed; - const showTransformForCrop = props.resize_mode === 'crop'; - const allWidgets = this.widgets.filter(w => - (w.name && typeof w.name === 'string' && w.name.endsWith(`_${layerName}`)) || - (w.value && typeof w.value === 'string' && w.value.endsWith(`_${layerName}`)) - ); - - for(const w of allWidgets) { - if (w.name.startsWith(`header_anchor_`)) continue; - let isHidden = isLayerCollapsed; - if (w.name.startsWith("spacer_for_")) { - isHidden = false; - } else if (w.name === `toggle_color_${layerName}`) { - } else if (["brightness", "contrast", "saturation", "color_r", "color_g", "color_b"].some(p => w.name.startsWith(p))) { - isHidden = isLayerCollapsed || isColorCollapsed; - } else if (["scale", "offset_x", "offset_y", "rotation"].some(p => w.name.startsWith(p))) { - isHidden = isLayerCollapsed || !showTransformForCrop; - } - - w.hidden = isHidden; - if (!w.originalComputeSize) w.originalComputeSize = w.computeSize; - w.computeSize = isHidden ? () => [0, -4] : w.originalComputeSize; - } - - resizeHeight.call(this); - }; - -nodeType.prototype.refreshUI = function() { - this.updateLayerWidgets(); - const activeLayerKeys = Object.keys(this.layer_properties); - const activeLayers = new Set(activeLayerKeys); - - const baseAnchor = this.widgets.find(w => w.name === `header_anchor_1`); - if (baseAnchor) { - if (this.base_image_properties) { - baseAnchor.hidden = false; - baseAnchor.computeSize = (width) => [width, 64]; - if (baseAnchor.canvas) { - this.drawHeaderCanvas(baseAnchor.canvas, 'base_image'); - } - } else { - baseAnchor.hidden = true; - baseAnchor.computeSize = () => [0, -4]; - } - } - - for (let i = 2; i <= MAX_LAYERS; i++) { - const layerName = `layer_${i - 1}`; - const anchor = this.widgets.find(w => w.name === `header_anchor_${i}`); - if (!anchor) continue; - if (activeLayers.has(layerName)) { - anchor.hidden = false; - anchor.computeSize = (width) => [width, 64]; - if (anchor.canvas) { - this.drawHeaderCanvas(anchor.canvas, layerName); - } - this.updateLayerVisibility(layerName); - } else { - anchor.hidden = true; - anchor.computeSize = () => [0, -4]; - } - } - - if (!this.base_image_properties && activeLayerKeys.length === 1) { - const layerName = activeLayerKeys[0]; - if (this.layer_properties[layerName]) { - this.layer_properties[layerName].layer_collapsed = false; - this.updateLayerVisibility(layerName); - } - } - - if (this.toolbar) { - if (this.toolbar.activeTool === 'mask') { - this.toolbar.maskManager.show(); - } else { - this.toolbar.maskManager.hide(); - } - } - - this.updatePropertiesJSON(); - resizeHeight.call(this); -}; - -nodeType.prototype.moveLayer = function(layer_index, direction) { - let layers_array = Object.entries(this.layer_properties).map(([name, props]) => { - const maskName = name.replace('layer_', 'mask_'); - return { - name, - props, - preview_img: this.loaded_preview_images ? this.loaded_preview_images[name] : null, - preview_data: this.preview_data ? this.preview_data[name] : null, - mask_img: this.loaded_preview_images ? this.loaded_preview_images[maskName] : null, - mask_data: this.preview_data ? this.preview_data[maskName] : null - }; - }); - - const fromIndex = layers_array.findIndex(l => l.name === `layer_${layer_index}`); - if (fromIndex === -1) return; - - const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1; - const element = layers_array.splice(fromIndex, 1)[0]; - layers_array.splice(toIndex, 0, element); - this.layer_properties = {}; - const old_loaded_preview_images = this.loaded_preview_images || {}; - const old_preview_data = this.preview_data || {}; - this.loaded_preview_images = { base_image: old_loaded_preview_images['base_image'] }; - this.preview_data = { base_image: old_preview_data['base_image'] }; - layers_array.forEach((layer, index) => { - const newLayerName = `layer_${index + 1}`; - const newMaskName = `mask_${index + 1}`; - this.layer_properties[newLayerName] = layer.props; - if (layer.preview_img) this.loaded_preview_images[newLayerName] = layer.preview_img; - if (layer.preview_data) this.preview_data[newLayerName] = layer.preview_data; - if (layer.mask_img) this.loaded_preview_images[newMaskName] = layer.mask_img; - if (layer.mask_data) this.preview_data[newMaskName] = layer.mask_data; - if (this.movingLayer === layer.name) { - this.movingLayer = newLayerName; - } - }); - this.updatePropertiesJSON(); - this.refreshUI(); - this.redrawPreviewCanvas(); -}; -nodeType.prototype.addLayerWidgets = function(layer_name) { - if (!this.layer_properties[layer_name]) { - this.layer_properties[layer_name] = { - blend_mode: "normal", opacity: 1.0, enabled: true, resize_mode: "crop", scale: 1.0, offset_x: 0, offset_y: 0, - 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: true, - internal_mask_filename: null, - internal_mask_details: null, - }; - } - const props = this.layer_properties[layer_name]; - const allWidgets = []; - const topSpacer = { name: `top_spacer_for_${layer_name}`, type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] }; - this.widgets.push(topSpacer); - allWidgets.push(topSpacer); - const redrawCallback = (prop, v) => { - props[prop] = v; - this.updatePropertiesJSON(); - this.redrawPreviewCanvas(); - this.graph.setDirtyCanvas(true, true); - }; - - allWidgets.push(this.addWidget("combo", `blend_mode_${layer_name}`, props.blend_mode, (v) => { props.blend_mode = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); }, {values: BLEND_MODES})); - allWidgets.push(this.addWidget("number", `opacity_${layer_name}`, props.opacity, (v) => { props.opacity = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); }, {min: 0.0, max: 1.0, step: 0.1, precision: 2})); - const colorToggle = this.addWidget("toggle", `toggle_color_${layer_name}`, !props.color_section_collapsed, (v) => { - props.color_section_collapsed = !v; this.refreshUI(); this.updatePropertiesJSON(); - }, { on: "▼ Color Adjust", off: "▶ Color Adjust" }); - allWidgets.push(colorToggle); - - allWidgets.push(this.addWidget("number", `brightness_${layer_name}`, props.brightness, (v) => redrawCallback('brightness', v), {min: -1.0, max: 1.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `contrast_${layer_name}`, props.contrast, (v) => redrawCallback('contrast', v), {min: -1.0, max: 1.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `saturation_${layer_name}`, props.saturation, (v) => redrawCallback('saturation', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `color_r_${layer_name}`, props.color_r, (v) => redrawCallback('color_r', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `color_g_${layer_name}`, props.color_g, (v) => redrawCallback('color_g', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `color_b_${layer_name}`, props.color_b, (v) => redrawCallback('color_b', v), {min: 0.0, max: 2.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("toggle", `invert_mask_${layer_name}`, !!props.invert_mask, (v) => { - props.invert_mask = v; this.updatePropertiesJSON(); this.redrawPreviewCanvas(); - })); - const resizeModeWidget = this.addWidget("combo", `resize_mode_${layer_name}`, props.resize_mode, (v) => { - props.resize_mode = v; if (v !== 'crop' && this.movingLayer === layer_name) { this.movingLayer = null; } - this.refreshUI(); - this.updatePropertiesJSON(); this.redrawPreviewCanvas(); - }, { values: RESIZE_MODES }); - allWidgets.push(resizeModeWidget); - allWidgets.push(this.addWidget("number", `scale_${layer_name}`, props.scale, (v) => redrawCallback('scale', v), {min: 0.01, max: 10.0, step: 0.1, precision: 2 })); - allWidgets.push(this.addWidget("number", `offset_x_${layer_name}`, props.offset_x, (v) => redrawCallback('offset_x', v), {min: -8192, max: 8192, step: 1 })); - allWidgets.push(this.addWidget("number", `offset_y_${layer_name}`, props.offset_y, (v) => redrawCallback('offset_y', v), {min: -8192, max: 8192, step: 1 })); - allWidgets.push(this.addWidget("number", `rotation_${layer_name}`, props.rotation, (v) => redrawCallback('rotation', v), {min: -360.0, max: 360.0, step: 1, precision: 1 })); - const bottomSpacer = { name: `bottom_spacer_for_${layer_name}`, type: "CUSTOM_SPACER", draw: () => {}, computeSize: () => [0, 10] }; - this.widgets.push(bottomSpacer); - allWidgets.push(bottomSpacer); - for (const w of allWidgets) { - if (!w.originalComputeSize) w.originalComputeSize = w.computeSize; - } -}; - -nodeType.prototype.updateLayerWidgets = function() { - this.widgets = this.widgets.filter(w => - w.name.includes("_anchor") || - w.name === "_properties_json" || - w.name === "Add Image" || - w.name === "global_top_spacer" - ); - - const activeLayerKeys = Object.keys(this.layer_properties); - activeLayerKeys.sort((a, b) => parseInt(a.split("_")[1]) - parseInt(b.split("_")[1])); - - for (const layerName of activeLayerKeys) { - this.addLayerWidgets(layerName); - } -}; - -nodeType.prototype.getActiveLayer = function() { - if (!this.layer_properties) return null; - const openLayerName = Object.keys(this.layer_properties).find(name => this.layer_properties[name]?.layer_collapsed === false); - if (openLayerName) { - const layerIndex = parseInt(openLayerName.split("_")[1]); - const displayIndex = this.layer_order ? this.layer_order.indexOf(openLayerName) + 1 : layerIndex; - return { name: openLayerName, index: layerIndex, displayIndex: displayIndex }; - } - return null; -}; - -nodeType.prototype.handleDisconnectedInputs = function() { - const connected_layer_names = new Set(this.inputs.filter(i => i.name.startsWith("layer_") && i.link !== null).map(i => i.name)); - const inputs_to_remove = []; - const props_to_remove = []; - for (const key in this.layer_properties) { - if (!connected_layer_names.has(key)) { - props_to_remove.push(key); - const layer_input = this.inputs.find(i => i.name === key); - if(layer_input) inputs_to_remove.push(layer_input); - const mask_input = this.inputs.find(i => i.name === key.replace("layer_", "mask_")); - if(mask_input) inputs_to_remove.push(mask_input); - } - } - props_to_remove.forEach(key => delete this.layer_properties[key]); - inputs_to_remove.sort((a,b) => this.inputs.indexOf(b) - this.inputs.indexOf(a)).forEach(i => this.removeInput(this.inputs.indexOf(i))); - - const final_props = {}; - const final_images = {}; - const final_data = {}; - for (const key in this.loaded_preview_images) { - if (!key.startsWith("layer_") && !key.startsWith("mask_")) { - final_images[key] = this.loaded_preview_images[key]; - } - } - if (this.preview_data) { - for (const key in this.preview_data) { - if (!key.startsWith("layer_") && !key.startsWith("mask_")) { - final_data[key] = this.preview_data[key]; - } - } - } - - const remaining_layers = this.inputs.filter(i => i.name.startsWith("layer_")); - remaining_layers.sort((a, b) => this.inputs.indexOf(a) - this.inputs.indexOf(b)); - remaining_layers.forEach((input, i) => { - const old_name = input.name; - const new_name = `layer_${i + 1}`; - const old_mask_name = old_name.replace("layer_", "mask_"); - const new_mask_name = new_name.replace("layer_", "mask_"); - if (this.layer_properties[old_name]) { - final_props[new_name] = this.layer_properties[old_name]; - } - - if (this.loaded_preview_images[old_name]) { - final_images[new_name] = this.loaded_preview_images[old_name]; - } - if (this.loaded_preview_images[old_mask_name]) { - final_images[new_mask_name] = this.loaded_preview_images[old_mask_name]; - } - if (this.preview_data && this.preview_data[old_name]) { - final_data[new_name] = this.preview_data[old_name]; - } - if (this.preview_data && this.preview_data[old_mask_name]) { - final_data[new_mask_name] = this.preview_data[old_mask_name]; - } - input.name = new_name; - const mask_input = this.inputs.find(m => m.name === old_mask_name); - if (mask_input) { - mask_input.name = new_mask_name; - } - }); - this.layer_properties = final_props; - this.loaded_preview_images = final_images; - this.preview_data = final_data; - - const finalLayerNames = Object.keys(this.layer_properties); - if (finalLayerNames.length > 0) { - const isAnyLayerExpanded = finalLayerNames.some(name => - !this.layer_properties[name].layer_collapsed - ); - if (!isAnyLayerExpanded) { - this.layer_properties[finalLayerNames[0]].layer_collapsed = false; - } - } -}; - - nodeType.prototype.ensureWildcardInputs = function () { - const layerInputs = this.inputs.filter(i => i.name.startsWith("layer_")); - if(layerInputs.length >= MAX_LAYERS) return; - const lastLayerInput = layerInputs.sort((a,b) => parseInt(a.name.split("_")[1]) - parseInt(b.name.split("_")[1])).pop(); - if (!lastLayerInput || lastLayerInput.link !== null) { - const newIndex = lastLayerInput ? parseInt(lastLayerInput.name.split("_")[1]) + 1 : 1; - if (newIndex > MAX_LAYERS) return; - this.addInput(`layer_${newIndex}`, "IMAGE"); - this.addInput(`mask_${newIndex}`, "MASK"); - } - }; - -nodeType.prototype.handleInternalImageLoad = function() { - if (Object.keys(this.layer_properties).length >= MAX_LAYERS) { - alert(`Maximum number of layers (${MAX_LAYERS}) reached.`); - return; - } - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = 'image/jpeg,image/png,image/webp'; - fileInput.style.display = 'none'; - - fileInput.onchange = async (e) => { - if (!e.target.files.length) { - document.body.removeChild(fileInput); - return; - } - const originalFile = e.target.files[0]; - - try { - const isBaseImage = !this.base_image_properties; - 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 --- - - // 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é - - 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.basePreviewImage = newImage; - } else { - const existingIndices = new Set(Object.keys(this.layer_properties).map(k => parseInt(k.split('_')[1]))); - let newIndex = 1; - while (existingIndices.has(newIndex)) { newIndex++; } - const layerName = `layer_${newIndex}`; - - if (this.accordionMode) { - for (const key in this.layer_properties) { - this.layer_properties[key].layer_collapsed = true; - } - } - this.layer_properties[layerName] = { - source_filename: data.name, - source_details: data, - blend_mode: "normal", opacity: 1.0, enabled: true, resize_mode: "crop", scale: 1.0, offset_x: 0, offset_y: 0, - 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.updatePropertiesJSON(); - this.refreshUI(); - this.redrawPreviewCanvas(); // On redessine l'aperçu localement - - // On ne lance PAS de rendu backend - // app.queuePrompt(); - - } catch (error) { - console.error("Error uploading file:", error); - } finally { - document.body.removeChild(fileInput); - } - }; - document.body.appendChild(fileInput); - fileInput.click(); -}; - -nodeType.prototype.deleteLayer = function(layerNameToDelete) { - if (this.movingLayer === layerNameToDelete) { - this.movingLayer = null; - } - - const layerToDeleteProps = this.layer_properties[layerNameToDelete]; - if (layerToDeleteProps && layerToDeleteProps.source_details) { - const fileDetails = layerToDeleteProps.source_details; - const deleteFileOnServer = async (details) => { - try { - if (!details || !details.name) return; - await fetch("/layersystem/delete_file", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ filename: details.name, subfolder: details.subfolder }), - }); - } catch (e) { console.error("Failed to delete file:", e); } - }; - deleteFileOnServer(fileDetails); - if (layerToDeleteProps.internal_mask_details) deleteFileOnServer(layerToDeleteProps.internal_mask_details); - if (layerToDeleteProps.internal_preview_mask_details) deleteFileOnServer(layerToDeleteProps.internal_preview_mask_details); - } - - const layersToKeep = []; - const sortedKeys = Object.keys(this.layer_properties).sort((a, b) => parseInt(a.split('_')[1]) - parseInt(b.split('_')[1])); - - for (const name of sortedKeys) { - if (name !== layerNameToDelete) { - const maskName = name.replace('layer_', 'mask_'); - layersToKeep.push({ - props: JSON.parse(JSON.stringify(this.layer_properties[name])), - preview_img: this.loaded_preview_images[name], - preview_data: this.preview_data[name] ? JSON.parse(JSON.stringify(this.preview_data[name])) : undefined, - mask_img: this.loaded_preview_images[maskName], - mask_data: this.preview_data[maskName] ? JSON.parse(JSON.stringify(this.preview_data[maskName])) : undefined, - }); - } - } - - const new_layer_properties = {}; - const new_loaded_preview_images = { base_image: this.loaded_preview_images.base_image }; - const new_preview_data = { base_image: this.preview_data.base_image }; - - layersToKeep.forEach((layer, index) => { - const newLayerName = `layer_${index + 1}`; - const newMaskName = `mask_${index + 1}`; - new_layer_properties[newLayerName] = layer.props; - if (layer.preview_img) new_loaded_preview_images[newLayerName] = layer.preview_img; - if (layer.preview_data) new_preview_data[newLayerName] = layer.preview_data; - if (layer.mask_img) new_loaded_preview_images[newMaskName] = layer.mask_img; - if (layer.mask_data) new_preview_data[newMaskName] = layer.mask_data; - }); - - this.layer_properties = new_layer_properties; - this.loaded_preview_images = new_loaded_preview_images; - this.preview_data = new_preview_data; - - this.updatePropertiesJSON(); - this.refreshUI(); - this.redrawPreviewCanvas(); -}; - - nodeType.prototype.logFullState = function(label) { - console.log(`\n\n--- DEBUG STATE: ${label} ---`); - console.log("PROPRIÉTÉS DES CALQUES:", JSON.parse(JSON.stringify(this.layer_properties))); - console.log("DONNÉES DE PREVIEW:", this.preview_data); - console.log("--- FIN DEBUG STATE ---\n\n"); -}; - -nodeType.prototype.updatePropertiesJSON = function() { - const mainDataWidget = this.widgets.find(w => w.name === "_layer_system_data" || w.name === "_properties_json"); - if (mainDataWidget) { - const full_properties = { - base: this.base_image_properties, - layers: this.layer_properties, - texts: this.toolbar ? this.toolbar.getTexts() : [], - preview_width: this.previewCanvas ? this.previewCanvas.width : 512, - preview_height: this.previewCanvas ? this.previewCanvas.height : 512, - toolbar_width: this.toolbar ? this.toolbar.width : 0 - }; - mainDataWidget.value = JSON.stringify(full_properties); - } - this.widgets.forEach(widget => { - if (widget.name.includes("_anchor")) { - widget.value = null; - } - }); - }; - }, -}); \ No newline at end of file From 06b00d3b2111015a5591d3425e719a7f57b6cabe Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 12:26:19 +0200 Subject: [PATCH 05/20] fix change layer --- js/layer_system_final.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/layer_system_final.js b/js/layer_system_final.js index 161f331..f7519b0 100644 --- a/js/layer_system_final.js +++ b/js/layer_system_final.js @@ -1012,6 +1012,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.hide(); + } const layerName = e.currentTarget.dataset.layerName; if (!layerName) { return; From 1991aeac865f39d550cccde48ad6c16c4d3b709f Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 16:59:41 +0200 Subject: [PATCH 06/20] fix change layer --- js/layer_system_final.js | 6 +++--- js/mask_painter_manager.js | 11 +++++++++++ js/toolbar.js | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/js/layer_system_final.js b/js/layer_system_final.js index f7519b0..993ecef 100644 --- a/js/layer_system_final.js +++ b/js/layer_system_final.js @@ -1012,9 +1012,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.hide(); - } + if (this.toolbar?.activeTool === 'mask_painter' && this.toolbar?.maskPainterManager) { + this.toolbar.maskPainterManager.switchLayer(); + } const layerName = e.currentTarget.dataset.layerName; if (!layerName) { return; diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index 53b8904..371d662 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -66,6 +66,7 @@ export class MaskPainterManager { } 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."); @@ -205,6 +206,16 @@ async show() { this.node.refreshUI(); } +async switchLayer() { + this.hide(); + + // On attend un tout petit peu pour laisser le temps à l'interface principale + // de traiter le clic et de changer le calque actif. + await new Promise(resolve => setTimeout(resolve, 0)); + + this.show(); +} + async finalizeDrawing() { const applyButton = this.toolbar.querySelector('[data-action="apply"]'); applyButton.textContent = "Applying..."; diff --git a/js/toolbar.js b/js/toolbar.js index 989ee58..7a8a84f 100644 --- a/js/toolbar.js +++ b/js/toolbar.js @@ -541,7 +541,7 @@ createSelectionSubMenu() { const paintMaskButton = createEmojiButton("🖌️", "Paint Mask"); // NOUVEAU paintMaskButton.onclick = () => { - this.activeTool = 'mask_painter'; + //this.activeTool = 'mask_painter'; if (this.maskPainterManager) { this.maskPainterManager.show(); } From a09c00759e570bda6a214bce3eab401dea8c7e54 Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 17:33:23 +0200 Subject: [PATCH 07/20] improve ui --- js/mask_painter_manager.js | 54 ++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index 371d662..dead954 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -21,33 +21,69 @@ export class MaskPainterManager { 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: '8px', color: 'white', fontFamily: 'sans-serif' + gap: '6px', color: 'white', fontFamily: 'sans-serif' }); + // --- CORRECTION : On retire le curseur Opacity --- this.toolbar.innerHTML = ` - + ${this.settings.size} + - + ${this.settings.hardness}% - - + + + + `; 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' ? '%' : ''; + const suffix = (setting === 'hardness') ? '%' : ''; this.toolbar.querySelector(`span[data-value="${setting}"]`).textContent = e.target.value + suffix; } }); @@ -55,11 +91,13 @@ export class MaskPainterManager { 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.querySelector('[data-mode="brush"]').style.border = `2px solid ${this.settings.mode === 'brush' ? 'white' : 'gray'}`; - this.toolbar.querySelector('[data-mode="eraser"]').style.border = `2px solid ${this.settings.mode === 'eraser' ? 'white' : 'gray'}`; + 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(); }); From c1b8851d0381596cfd07755274c1ccc05274f267 Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 17:47:40 +0200 Subject: [PATCH 08/20] fix cursor on toolbar --- js/mask_painter_manager.js | 72 ++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index dead954..a847422 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -340,52 +340,56 @@ async finalizeDrawing() { }; } - handleMouseEvent(e) { - if (!this.activeLayer) return; +handleMouseEvent(e) { + if (!this.activeLayer) return; // --- DÉBUT DE LA CORRECTION --- - // On calcule la position du clic par rapport au canvas principal en dessous + // On calcule la position de la souris 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; + const mouseX = (e.clientX - mainCanvasRect.left) / zoom; + const mouseY = (e.clientY - mainCanvasRect.top) / zoom; + const isOverToolbar = this.node.toolbar.isClickOnToolbar(mouseX, mouseY); - // Si c'est un clic sur la barre d'outils principale, on lui passe l'événement et on s'arrête. - if (e.type === 'mousedown' && this.node.toolbar.isClickOnToolbar(clickX_on_main_canvas, clickY_on_main_canvas)) { - this.node.toolbar.handleClick(e, clickX_on_main_canvas, clickY_on_main_canvas); - return; - } - // --- FIN DE LA CORRECTION --- - + // Si la souris est sur la barre d'outils principale... + if (isOverToolbar) { + // On réaffiche le curseur normal de la souris + this.liveOverlay.style.cursor = 'default'; + + // On met à jour l'aperçu SANS dessiner le curseur personnalisé (en passant 'null') + this.updateLivePreview(null); + + // Si on clique, on passe l'événement à la barre d'outils et on s'arrête là 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); - this.drawStroke(this.lastPoint, coords); - this.lastPoint = coords; - } - } else if (e.type === 'mouseup' || e.type === 'mouseleave') { - this.isDrawing = false; + this.node.toolbar.handleClick(e, mouseX, mouseY); } - this.updateLivePreview(e); + return; // On stoppe tout autre traitement } - 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 }); + // Si on n'est PAS sur la barre d'outils, on cache le curseur normal + this.liveOverlay.style.cursor = 'none'; + // --- FIN DE LA CORRECTION --- + + + 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); + this.drawStroke(this.lastPoint, coords); + this.lastPoint = coords; } - this.drawStamp(to); + } else if (e.type === 'mouseup' || e.type === 'mouseleave') { + this.isDrawing = false; } + // On passe l'événement pour que l'aperçu sache où dessiner le curseur personnalisé + this.updateLivePreview(e); +} + updateLivePreview(mouseEvent = null) { if (!this.liveCtx) return; this.liveCtx.clearRect(0, 0, this.liveOverlay.width, this.liveOverlay.height); From 452f558ee2ad4f12dbde6da91118580294ecf456 Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 19:13:29 +0200 Subject: [PATCH 09/20] fix draw freeze --- js/mask_painter_manager.js | 41 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index a847422..f21ad9c 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -343,53 +343,60 @@ async finalizeDrawing() { handleMouseEvent(e) { if (!this.activeLayer) return; - // --- DÉBUT DE LA CORRECTION --- - // On calcule la position de la souris + // On calcule la position et on vérifie si on est sur la barre d'outils 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); - // Si la souris est sur la barre d'outils principale... + // CAS 1 : La souris est sur la barre d'outils principale if (isOverToolbar) { - // On réaffiche le curseur normal de la souris - this.liveOverlay.style.cursor = 'default'; - - // On met à jour l'aperçu SANS dessiner le curseur personnalisé (en passant 'null') - this.updateLivePreview(null); + this.liveOverlay.style.cursor = 'default'; // On montre le curseur normal + this.updateLivePreview(null); // On met à jour l'aperçu sans le curseur personnalisé - // Si on clique, on passe l'événement à la barre d'outils et on s'arrête là if (e.type === 'mousedown') { this.node.toolbar.handleClick(e, mouseX, mouseY); } - return; // On stoppe tout autre traitement + return; // On s'arrête ici } - - // Si on n'est PAS sur la barre d'outils, on cache le curseur normal - this.liveOverlay.style.cursor = 'none'; - // --- FIN DE LA CORRECTION --- + // CAS 2 : La souris est sur la zone de dessin + this.liveOverlay.style.cursor = 'none'; // On cache le curseur normal pour dessiner le nôtre if (e.type === 'mousedown') { this.isDrawing = true; const coords = this.getOriginalCoords(e); this.lastPoint = coords; this.drawStamp(coords); - } else if (e.type === 'mousemove') { + } + else if (e.type === 'mousemove') { if (this.isDrawing) { const coords = this.getOriginalCoords(e); this.drawStroke(this.lastPoint, coords); this.lastPoint = coords; } - } else if (e.type === 'mouseup' || e.type === 'mouseleave') { + } + else if (e.type === 'mouseup' || e.type === 'mouseleave') { this.isDrawing = false; } - // On passe l'événement pour que l'aperçu sache où dessiner le curseur personnalisé + // On met à jour l'aperçu et le curseur personnalisé 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); From 419fed60f7bddf0b599bf1731683b531d74c637e Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 19:14:21 +0200 Subject: [PATCH 10/20] fix clean input rep --- layer_system_final.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/layer_system_final.py b/layer_system_final.py index 56ac4f8..1bae4bb 100644 --- a/layer_system_final.py +++ b/layer_system_final.py @@ -703,26 +703,25 @@ async def finalize_painter_mask_route(request): alpha_mask_pil = Image.open(alpha_mask_path) alpha_channel = alpha_mask_pil.getchannel('A') - # --- DÉBUT DE LA CORRECTION --- - - # 1. Crée le masque d'aperçu (Blanc = Visible) en format RGB preview_mask_pil = Image.new("RGB", alpha_channel.size, "black") preview_mask_pil.paste((255, 255, 255), mask=alpha_channel) - # 2. Crée le masque de rendu (Noir = Visible) en format RGB, comme les autres outils render_mask_pil = ImageOps.invert(preview_mask_pil.convert("L")).convert("RGB") - # --- FIN DE LA CORRECTION --- - output_dir = folder_paths.get_input_directory() - # On ajoute un timestamp pour éviter les problèmes de cache navigateur - timestamp = int(time.time() * 1000) - preview_filename = f"internal_mask_preview_{layer_index}_{timestamp}.png" - render_filename = f"internal_mask_render_{layer_index}_{timestamp}.png" + + # --- CORRECTION : On retire le timestamp pour écraser les fichiers --- + preview_filename = f"internal_mask_preview_{layer_index}.png" + render_filename = f"internal_mask_render_{layer_index}.png" + # --- FIN CORRECTION --- preview_mask_pil.save(os.path.join(output_dir, preview_filename), "PNG") render_mask_pil.save(os.path.join(output_dir, render_filename), "PNG") + # On peut supprimer le fichier alpha temporaire qui ne sert plus à rien + 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" }, @@ -733,7 +732,7 @@ async def finalize_painter_mask_route(request): import traceback print(f"[Layer System] ERREUR API finalize_painter_mask: {e}") traceback.print_exc() - return web.Response(status=500, text=str(e)) + return web.Response(status=500, text=str(e)) NODE_CLASS_MAPPINGS = { "LayerSystem": LayerSystem } From d0787148fc4f3306c03492e11418ea2b88373bfd Mon Sep 17 00:00:00 2001 From: tritant Date: Mon, 6 Oct 2025 21:57:41 +0200 Subject: [PATCH 11/20] add opacity slider --- js/mask_painter_manager.js | 235 ++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 124 deletions(-) diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index f21ad9c..4a1a609 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -5,7 +5,8 @@ export class MaskPainterManager { this.lastPoint = { x: 0, y: 0 }; this.settings = { size: 30, - hardness: 80, // Le retour de la dureté ! + hardness: 80, + opacity: 100, // Le retour de la dureté ! mode: 'brush' }; @@ -19,90 +20,71 @@ export class MaskPainterManager { this.createSettingsToolbar(); } - createSettingsToolbar() { - if (this.toolbar) this.toolbar.remove(); +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' - }); - - // --- CORRECTION : On retire le curseur Opacity --- - this.toolbar.innerHTML = ` - - - ${this.settings.size} - - - - ${this.settings.hardness}% - - - - - - + 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.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') ? '%' : ''; - 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(); - }); + 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: '8px', 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(); @@ -192,37 +174,37 @@ async show() { 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); - // La dureté (0-100) contrôle où le dégradé devient transparent - const hardnessStop = Math.max(0, Math.min(1, this.settings.hardness / 100)); - - // Pinceau (Révéler) -> Peint de l'opaque avec 'source-over' - if (this.settings.mode === 'brush') { - gradient.addColorStop(0, 'white'); - gradient.addColorStop(hardnessStop, 'white'); - gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); - ctx.globalCompositeOperation = 'source-over'; - ctx.fillStyle = gradient; - - // Gomme (Cacher) -> Efface vers le transparent avec 'destination-out' - } else { - gradient.addColorStop(0, 'black'); // La couleur n'importe pas - 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.globalCompositeOperation = 'source-over'; - } +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)'); + // On revient à 'lighter' + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = gradient; + } else { // La gomme reste correcte + 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]; @@ -343,45 +325,50 @@ async finalizeDrawing() { handleMouseEvent(e) { if (!this.activeLayer) return; - // On calcule la position et on vérifie si on est sur la barre d'outils + // La gestion de la barre d'outils reste la même 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); - // CAS 1 : La souris est sur la barre d'outils principale if (isOverToolbar) { - this.liveOverlay.style.cursor = 'default'; // On montre le curseur normal - this.updateLivePreview(null); // On met à jour l'aperçu sans le curseur personnalisé - + this.liveOverlay.style.cursor = 'default'; + this.updateLivePreview(null); if (e.type === 'mousedown') { this.node.toolbar.handleClick(e, mouseX, mouseY); } - return; // On s'arrête ici + return; } - - // CAS 2 : La souris est sur la zone de dessin - this.liveOverlay.style.cursor = 'none'; // On cache le curseur normal pour dessiner le nôtre + + this.liveOverlay.style.cursor = 'none'; if (e.type === 'mousedown') { this.isDrawing = true; const coords = this.getOriginalCoords(e); this.lastPoint = coords; - this.drawStamp(coords); + this.drawStamp(coords); // On dessine le premier point } else if (e.type === 'mousemove') { if (this.isDrawing) { const coords = this.getOriginalCoords(e); - this.drawStroke(this.lastPoint, coords); - this.lastPoint = coords; + + // --- TA SOLUTION : L'ESPACEMENT --- + const dist = Math.hypot(coords.x - this.lastPoint.x, coords.y - this.lastPoint.y); + const spacing = this.settings.size / 2; // On peut ajuster cette valeur + + // On ne dessine un nouveau segment que si on a bougé suffisamment + if (dist >= spacing) { + this.drawStroke(this.lastPoint, coords); + this.lastPoint = coords; // On met à jour la position du dernier point DESSINÉ + } + // --- FIN DE LA SOLUTION --- } } else if (e.type === 'mouseup' || e.type === 'mouseleave') { this.isDrawing = false; } - // On met à jour l'aperçu et le curseur personnalisé this.updateLivePreview(e); } From 4dec1b55f7317f645e8942289cadf5a5fac4bb59 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 12:44:21 +0200 Subject: [PATCH 12/20] code cleanup --- layer_system_final.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/layer_system_final.py b/layer_system_final.py index 1bae4bb..9ef5962 100644 --- a/layer_system_final.py +++ b/layer_system_final.py @@ -676,15 +676,12 @@ 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)) @@ -710,15 +707,12 @@ async def finalize_painter_mask_route(request): output_dir = folder_paths.get_input_directory() - # --- CORRECTION : On retire le timestamp pour écraser les fichiers --- preview_filename = f"internal_mask_preview_{layer_index}.png" render_filename = f"internal_mask_render_{layer_index}.png" - # --- FIN CORRECTION --- preview_mask_pil.save(os.path.join(output_dir, preview_filename), "PNG") render_mask_pil.save(os.path.join(output_dir, render_filename), "PNG") - # On peut supprimer le fichier alpha temporaire qui ne sert plus à rien if os.path.exists(alpha_mask_path): os.remove(alpha_mask_path) From 0920f0fff3a708174a65335795d5d1c608d7fa9d Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 12:56:54 +0200 Subject: [PATCH 13/20] code cleanup --- js/layer_system_final.js | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/js/layer_system_final.js b/js/layer_system_final.js index 993ecef..0fedf36 100644 --- a/js/layer_system_final.js +++ b/js/layer_system_final.js @@ -356,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); @@ -396,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 }); } @@ -1637,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]))); @@ -1676,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); From 727a2381b12d81d9144540dcff1614aa60620b6a Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:00:07 +0200 Subject: [PATCH 14/20] code cleanup --- js/toolbar.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/toolbar.js b/js/toolbar.js index 7a8a84f..42bf47d 100644 --- a/js/toolbar.js +++ b/js/toolbar.js @@ -539,9 +539,8 @@ createSelectionSubMenu() { this.selectionSubMenu.style.display = 'none'; }; -const paintMaskButton = createEmojiButton("🖌️", "Paint Mask"); // NOUVEAU -paintMaskButton.onclick = () => { - //this.activeTool = 'mask_painter'; +const paintMaskButton = createEmojiButton("🖌️", "Paint Mask"); + if (this.maskPainterManager) { this.maskPainterManager.show(); } From f8e853d0650c93a861be52609f1bedfea2e92e84 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:04:18 +0200 Subject: [PATCH 15/20] code cleanup --- js/removebg.js | 1 - 1 file changed, 1 deletion(-) 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; From 6f0e1b3bb5480a31baa0fa96edbf2090aa9fd1a7 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:06:30 +0200 Subject: [PATCH 16/20] code cleanup --- js/magic_wand_manager.js | 2 -- 1 file changed, 2 deletions(-) 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)); From 808f9929409195ac9de8ad6f0883a9feb8969dfd Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:17:14 +0200 Subject: [PATCH 17/20] code cleanup --- js/brush_manager.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) 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')); From ea91ecd5b4fadac3483ef46b4c0416193a24d6e4 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:26:47 +0200 Subject: [PATCH 18/20] code cleanup --- js/mask_painter_manager.js | 58 +++++++++++++------------------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/js/mask_painter_manager.js b/js/mask_painter_manager.js index 4a1a609..4dded1c 100644 --- a/js/mask_painter_manager.js +++ b/js/mask_painter_manager.js @@ -6,7 +6,7 @@ export class MaskPainterManager { this.settings = { size: 30, hardness: 80, - opacity: 100, // Le retour de la dureté ! + opacity: 100, mode: 'brush' }; @@ -41,22 +41,22 @@ createSettingsToolbar() { position: 'fixed', display: 'none', zIndex: '10002', backgroundColor: 'rgba(40, 40, 40, 0.9)', border: '1px solid #555', borderRadius: '8px', padding: '8px', alignItems: 'center', - gap: '8px', color: 'white', fontFamily: 'sans-serif' + gap: '6px', color: 'white', fontFamily: 'sans-serif' }); this.toolbar.innerHTML = ` - - ${this.settings.size} - - - ${this.settings.hardness}% - - - ${this.settings.opacity}% + + ${this.settings.size} + + + ${this.settings.hardness}% + + + ${this.settings.opacity}% - + `; document.body.appendChild(this.toolbar); @@ -96,7 +96,6 @@ async show() { const layerImage = this.node.loaded_preview_images[this.activeLayer.name]; if (!layerImage || !layerImage.naturalWidth) return; - // On éteint le calque dans le rendu principal const props = this.node.layer_properties[this.activeLayer.name]; props.enabled = false; this.node.refreshUI(); @@ -149,22 +148,19 @@ async show() { } if (hasAlpha) { - // CAS 1 : Masque de l'éditeur (RGBA). Ton test prouve qu'il faut inverser sa transparence. for (let i = 0; i < data.length; i += 4) { - data[i + 3] = 255 - data[i + 3]; // On inverse le canal alpha existant - data[i] = 255; data[i + 1] = 255; data[i + 2] = 255; // Et on normalise le RGB + data[i + 3] = 255 - data[i + 3]; + data[i] = 255; data[i + 1] = 255; data[i + 2] = 255; } } else { - // CAS 2 : Masque de notre outil (RGB Noir/Blanc). Cette logique est correcte et n'inverse pas. for (let i = 0; i < data.length; i += 4) { - data[i + 3] = 255 - data[i]; // alpha = 255 - luminance + data[i + 3] = 255 - data[i]; data[i] = data[i+1] = data[i+2] = 255; } } this.maskCtx.putImageData(imageData, 0, 0); } else { - // Un nouveau masque est entièrement opaque (calque visible). this.maskCtx.fillStyle = 'white'; this.maskCtx.fillRect(0, 0, this.maskCanvas.width, this.maskCanvas.height); } @@ -188,10 +184,9 @@ drawStamp(point) { gradient.addColorStop(0, 'white'); gradient.addColorStop(hardnessStop, 'white'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); - // On revient à 'lighter' ctx.globalCompositeOperation = 'lighter'; ctx.fillStyle = gradient; - } else { // La gomme reste correcte + } else { gradient.addColorStop(0, 'black'); gradient.addColorStop(hardnessStop, 'black'); gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); @@ -209,7 +204,7 @@ drawStamp(point) { if (this.activeLayer) { const props = this.node.layer_properties[this.activeLayer.name]; if (props) { - props.enabled = true; // On rallume le calque + props.enabled = true; this.node.updatePropertiesJSON(); } } @@ -228,11 +223,7 @@ drawStamp(point) { async switchLayer() { this.hide(); - - // On attend un tout petit peu pour laisser le temps à l'interface principale - // de traiter le clic et de changer le calque actif. await new Promise(resolve => setTimeout(resolve, 0)); - this.show(); } @@ -247,11 +238,7 @@ async finalizeDrawing() { const formData = new FormData(); formData.append("image", file); formData.append("overwrite", "true"); - - // --- CORRECTION --- - // On sauvegarde le fichier temporaire dans 'input' au lieu de 'temp' formData.append("type", "input"); - // --- FIN CORRECTION --- let response = await fetch("/upload/image", { method: "POST", body: formData }); const tempAlphaMaskDetails = await response.json(); @@ -325,7 +312,6 @@ async finalizeDrawing() { handleMouseEvent(e) { if (!this.activeLayer) return; - // La gestion de la barre d'outils reste la même const mainCanvasRect = this.node.previewCanvas.getBoundingClientRect(); const zoom = app.canvas.ds.scale || 1; const mouseX = (e.clientX - mainCanvasRect.left) / zoom; @@ -347,22 +333,18 @@ handleMouseEvent(e) { this.isDrawing = true; const coords = this.getOriginalCoords(e); this.lastPoint = coords; - this.drawStamp(coords); // On dessine le premier point + this.drawStamp(coords); } else if (e.type === 'mousemove') { if (this.isDrawing) { const coords = this.getOriginalCoords(e); - - // --- TA SOLUTION : L'ESPACEMENT --- const dist = Math.hypot(coords.x - this.lastPoint.x, coords.y - this.lastPoint.y); - const spacing = this.settings.size / 2; // On peut ajuster cette valeur + const spacing = this.settings.size / 2; - // On ne dessine un nouveau segment que si on a bougé suffisamment if (dist >= spacing) { this.drawStroke(this.lastPoint, coords); - this.lastPoint = coords; // On met à jour la position du dernier point DESSINÉ + this.lastPoint = coords; } - // --- FIN DE LA SOLUTION --- } } else if (e.type === 'mouseup' || e.type === 'mouseleave') { @@ -388,8 +370,6 @@ handleMouseEvent(e) { if (!this.liveCtx) return; this.liveCtx.clearRect(0, 0, this.liveOverlay.width, this.liveOverlay.height); - // On ne dessine plus le fond, il est déjà visible en dessous ! - const tempLayerCanvas = document.createElement('canvas'); tempLayerCanvas.width = this.maskCanvas.width; tempLayerCanvas.height = this.maskCanvas.height; From 51d267049b6a9a4257c9a4df392dab6ed491d283 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:28:15 +0200 Subject: [PATCH 19/20] code cleanup From 92038635613483327967a3c98b799ae10fb08240 Mon Sep 17 00:00:00 2001 From: tritant Date: Tue, 7 Oct 2025 13:29:23 +0200 Subject: [PATCH 20/20] code cleanup --- js/mask_manager.js | 2 -- 1 file changed, 2 deletions(-) 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 => {