From 729a026e38f6b9a92445f4d0944ca8ea97c1944f Mon Sep 17 00:00:00 2001 From: tritant Date: Fri, 19 Sep 2025 18:23:00 +0200 Subject: [PATCH] Add magic selection --- js/layer_system_final.js | 62 ++++++- js/magic_wand_manager.js | 346 +++++++++++++++++++++++++++++++++++++++ js/mask_manager.js | 20 +-- js/removebg.js | 2 +- js/toolbar.js | 184 ++++++++++++++++++--- 5 files changed, 565 insertions(+), 49 deletions(-) create mode 100644 js/magic_wand_manager.js diff --git a/js/layer_system_final.js b/js/layer_system_final.js index b5f4d22..7d16434 100644 --- a/js/layer_system_final.js +++ b/js/layer_system_final.js @@ -102,6 +102,12 @@ app.registerExtension({ } 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(); } } }; @@ -119,6 +125,12 @@ app.registerExtension({ } if (this.toolbar?.maskManager?.contextualToolbar) { this.toolbar.maskManager.contextualToolbar.remove(); + } + if (this.toolbar.selectionSubMenu) { // Le sous-menu de choix d'outil + this.toolbar.selectionSubMenu.remove(); + } + if (this.toolbar.magicWandManager?.settingsToolbar) { // La barre de réglages de la baguette + this.toolbar.magicWandManager.settingsToolbar.remove(); } }; @@ -175,6 +187,16 @@ this.widgets.push(topSpacer); container.appendChild(canvas); this.previewCanvas = canvas; this.previewCtx = canvas.getContext("2d"); + + const overlayCanvas = document.createElement("canvas"); + overlayCanvas.id = "ls-overlay-canvas"; // On lui donne un ID + overlayCanvas.style.position = "absolute"; + overlayCanvas.style.top = "0"; + overlayCanvas.style.left = "0"; + overlayCanvas.style.pointerEvents = "none"; // Très important: pour que les clics passent à travers + container.appendChild(overlayCanvas); + this.overlayCanvas = overlayCanvas; + const anchorIndex = this.widgets.indexOf(anchorWidget); if (anchorIndex > 0) { this.widgets.splice(anchorIndex, 1); @@ -184,6 +206,15 @@ this.widgets.push(topSpacer); this.previewCanvas.addEventListener('mousedown', (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; + + // PRIORITÉ N°1 : Le clic est-il sur une icône de la barre d'outils principale ? + // C'est la priorité absolue pour pouvoir changer d'outil à tout moment. + if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { + this.toolbar.handleClick(e, mouseX, mouseY); + return; + } + + // PRIORITÉ N°2 : Sommes-nous dans un mode d'action spécial (comme le déplacement de texte) ? if (this.textActionMode === 'moving' && this.activeTextObject) { const targetText = this.findTextElementAtPos(mouseX, mouseY); if (targetText && targetText.id === this.activeTextObject.id) { @@ -197,15 +228,19 @@ this.previewCanvas.addEventListener('mousedown', (e) => { e.stopPropagation(); return; } - if (this.toolbar.isClickOnToolbar(mouseX, mouseY)) { - this.toolbar.handleClick(e, mouseX, mouseY); + + // PRIORITÉ N°3 : Un outil est-il actif pour un clic sur la zone de l'image ? + // Ici, on met la baguette magique, puis les autres outils. + if (this.toolbar.activeTool === 'magic_wand' && this.toolbar.magicWandManager) { + this.toolbar.magicWandManager.handleCanvasClick(e); return; } - - if (this.toolbar.activeTool) { + if (this.toolbar.activeTool) { // Gère les autres outils comme le texte this.toolbar.handleCanvasClick(e); return; } + + // PRIORITÉ N°4 : Si rien de tout ça, est-ce une action sur un calque (déplacement, etc.) ? if (!this.movingLayer) return; const props = this.layer_properties[this.movingLayer]; if (!props) return; @@ -1004,7 +1039,7 @@ nodeType.prototype.initializeHeaderCanvases = function() { } 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)) { + } 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 { @@ -1142,10 +1177,14 @@ canvas.dataset.layerName = layerName; 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" : (props.resize_mode !== 'crop' || (this.toolbar && this.toolbar.activeTool) ? "#555" : LiteGraph.WIDGET_TEXT_COLOR); + + ctx.strokeStyle = isMoving ? "#F44" : (isMoveDisabled ? "#555" : LiteGraph.WIDGET_TEXT_COLOR); + //ctx.strokeStyle = isMoving ? "#F44" : (props.resize_mode !== 'crop' || (this.toolbar && this.toolbar.activeTool) ? "#555" : LiteGraph.WIDGET_TEXT_COLOR); ctx.lineWidth = 2; ctx.stroke(moveIconPath); ctx.restore(); @@ -1434,6 +1473,17 @@ nodeType.prototype.updateLayerWidgets = function() { } }; +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 = []; diff --git a/js/magic_wand_manager.js b/js/magic_wand_manager.js new file mode 100644 index 0000000..4b71d89 --- /dev/null +++ b/js/magic_wand_manager.js @@ -0,0 +1,346 @@ +export class MagicWandManager { + constructor(node) { + this.node = node; + this.contextualToolbar = null; + this.activeMaskPreview = null; + //this.lineDashOffset = 0; + // On stocke les réglages de l'outil ici + this.settings = { + tolerance: 32, + contiguous: true, + fusionMode: 'add' + }; + this.createContextualToolbar(); + } + +async handleCanvasClick(e) { + this.hideSelectionPreview(); + const activeLayer = this.node.getActiveLayer(); + if (!activeLayer) return; + + const layerName = activeLayer.name; + const props = this.node.layer_properties[layerName]; + const layerImage = this.node.loaded_preview_images[layerName]; + const baseImage = this.node.basePreviewImage; + const preview = this.node.previewCanvas; + const toolbar = this.node.toolbar; + + if (!props || !layerImage || !baseImage || !preview || !toolbar) return; + + const previewCanvasScale = this.node.previewCanvasScale || 1.0; + 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 clickX = e.offsetX; + const clickY = e.offsetY; + const dx = clickX - centerX; + const dy = clickY - centerY; + + const angleRad = - (props.rotation || 0) * Math.PI / 180; + const cosA = Math.cos(angleRad); + const sinA = Math.sin(angleRad); + const unrotatedDx = dx * cosA - dy * sinA; + const unrotatedDy = dx * sinA + dy * cosA; + + if (Math.abs(unrotatedDx) > transformedWidth / 2 || Math.abs(unrotatedDy) > transformedHeight / 2) { + return; + } + + const localX = unrotatedDx + transformedWidth / 2; + const localY = unrotatedDy + transformedHeight / 2; + + const finalX = Math.round(localX * (layerImage.naturalWidth / transformedWidth)); + const finalY = Math.round(localY * (layerImage.naturalHeight / transformedHeight)); + + const applyButton = this.contextualToolbar.querySelector("button"); + applyButton.innerText = "Processing..."; + applyButton.disabled = true; + + const dataToSend = { + filename: props.source_filename, + details: props.source_details, + x: finalX, + y: finalY, + tolerance: this.settings.tolerance, + contiguous: this.settings.contiguous, + }; + + try { + const response = await fetch("/layersystem/magic_wand", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(dataToSend), + }); + const result = await response.json(); + + if (result.success && result.mask_details) { + const maskUrl = new URL("/view", window.location.origin); + maskUrl.searchParams.set("filename", result.mask_details.name); + maskUrl.searchParams.set("type", result.mask_details.type); + maskUrl.searchParams.set("t", Date.now()); + + const maskImage = new Image(); + maskImage.src = maskUrl.href; + maskImage.onload = () => { + this.activeMaskPreview = maskImage; + this.showSelectionPreview(maskImage, props, layerImage); + }; + } + } catch (error) { + console.error("Erreur lors de l'appel à la baguette magique:", error); + } finally { + applyButton.innerText = "Apply mask"; + applyButton.disabled = false; + } +} + +showSelectionPreview(maskImage, props, layerImage) { + const overlay = this.node.overlayCanvas; + const preview = this.node.previewCanvas; + if (!overlay || !maskImage || !props || !layerImage) return; + + const ctx = overlay.getContext('2d'); + overlay.width = preview.width; + overlay.height = preview.height; + ctx.clearRect(0, 0, overlay.width, overlay.height); + + // --- Étape 1 : On crée l'image du surlignage, pixel par pixel --- + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); + tempCanvas.width = maskImage.naturalWidth; + tempCanvas.height = maskImage.naturalHeight; + tempCtx.drawImage(maskImage, 0, 0); + const maskData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + const highlightData = tempCtx.createImageData(tempCanvas.width, tempCanvas.height); + + for (let i = 0; i < maskData.data.length; i += 4) { + if (maskData.data[i] > 128) { // Si le pixel du masque est blanc... + highlightData.data[i] = 255; // R + highlightData.data[i + 1] = 0; // G + highlightData.data[i + 2] = 0; // B + highlightData.data[i + 3] = 102; // Alpha (opacité de 40%) + } + } + // On met notre surlignage rouge sur le canvas temporaire + tempCtx.putImageData(highlightData, 0, 0); + + // --- Étape 2 : On dessine ce surlignage avec les bonnes transformations --- + const previewCanvasScale = this.node.previewCanvasScale || 1.0; + const imageAreaCenterX = this.node.toolbar.width + (preview.width - this.node.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; + + ctx.save(); + ctx.translate(centerX, centerY); + ctx.rotate(angleRad); + + // On dessine notre canvas temporaire (qui contient le surlignage) avec les transformations + ctx.drawImage(tempCanvas, -transformedWidth / 2, -transformedHeight / 2, transformedWidth, transformedHeight); + + ctx.restore(); +} + +hideSelectionPreview() { + const overlay = this.node.overlayCanvas; + if (overlay) { + const ctx = overlay.getContext('2d'); + ctx.clearRect(0, 0, overlay.width, overlay.height); + } +} + + async _uploadFile(file, isTemp = true) { + const formData = new FormData(); + const type = isTemp ? "temp" : "input"; + formData.append("image", file); + formData.append("overwrite", "true"); + formData.append("type", type); + const response = await fetch("/upload/image", { method: "POST", body: formData }); + if (response.status !== 200) throw new Error(`Upload failed: ${response.status}`); + return await response.json(); + } + + createContextualToolbar() { + if (this.contextualToolbar) this.contextualToolbar.remove(); + this.contextualToolbar = document.createElement("div"); + Object.assign(this.contextualToolbar.style, { + position: 'fixed', + display: 'none', + top: '20px', // Position temporaire + left: '20px', // Position temporaire + zIndex: '10001', + backgroundColor: 'rgba(40, 40, 40, 0.9)', + border: '1px solid #555', + borderRadius: '8px', + padding: '10px', + //display: 'flex', + alignItems: 'center', + gap: '6px', + fontFamily: 'sans-serif', + fontSize: '14px', + color: 'white', + }); + + // --- Slider de Tolérance --- + const toleranceLabel = document.createElement("label"); + toleranceLabel.innerText = "Tolerance :"; + const toleranceInput = document.createElement("input"); + toleranceInput.type = "range"; + toleranceInput.min = "0"; + toleranceInput.max = "255"; + toleranceInput.value = this.settings.tolerance; + toleranceInput.style.width = '80px'; + toleranceInput.oninput = (e) => { + this.settings.tolerance = parseInt(e.target.value, 10); + toleranceValue.innerText = this.settings.tolerance; + }; + const toleranceValue = document.createElement("span"); + toleranceValue.innerText = this.settings.tolerance; + toleranceValue.style.minWidth = "25px"; + + // --- Case à cocher "Contigu" --- + const contiguousLabel = document.createElement("label"); + contiguousLabel.innerText = "Contigu :"; + const contiguousInput = document.createElement("input"); + contiguousInput.type = "checkbox"; + contiguousInput.checked = this.settings.contiguous; + contiguousInput.onchange = (e) => { + this.settings.contiguous = e.target.checked; + }; + + const modeLabel = document.createElement("label"); + modeLabel.innerText = "Mode :"; + Object.assign(modeLabel.style, { marginLeft: '10px' }); + + const modeSelect = document.createElement("select"); + modeSelect.innerHTML = ` + + + + `; + modeSelect.value = this.settings.fusionMode; + modeSelect.style.backgroundColor = "#333"; + modeSelect.style.color = "white"; + modeSelect.onchange = (e) => { + this.settings.fusionMode = e.target.value; + }; + + // --- Bouton "Appliquer" --- + const applyButton = document.createElement("button"); + applyButton.innerText = "Apply mask"; + applyButton.onclick = async () => { + if (!this.activeMaskPreview) { + alert("Aucune sélection à appliquer."); + return; + } + const activeLayer = this.node.getActiveLayer(); + if (!activeLayer) return; + + applyButton.innerText = "Processing..."; + applyButton.disabled = true; + + try { + // 1. On transforme notre image de preview en un fichier temporaire sur le serveur + const tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.activeMaskPreview.naturalWidth; + tempCanvas.height = this.activeMaskPreview.naturalHeight; + tempCanvas.getContext('2d').drawImage(this.activeMaskPreview, 0, 0); + const blob = await new Promise(resolve => tempCanvas.toBlob(resolve, 'image/png')); + const tempFile = new File([blob], `temp_selection_mask.png`, { type: "image/png" }); + const newMaskDetails = await this._uploadFile(tempFile, false); + + // 2. On récupère le nom du masque déjà existant sur le calque + const props = this.node.layer_properties[activeLayer.name]; + const existingMaskFilename = props.internal_mask_filename || null; + + // 3. On appelle l'API Python pour fusionner les deux masques + const response = await fetch("/layersystem/apply_mask", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + new_mask_details: newMaskDetails, + existing_mask_filename: existingMaskFilename, + fusion_mode: this.settings.fusionMode, + layer_index: activeLayer.index + }), + }); + const result = await response.json(); + + if (result.success && result.render_mask_details && result.preview_mask_details && result.editor_mask_details) { + + // On met à jour les propriétés du calque avec les 3 nouveaux masques + props.internal_mask_filename = result.render_mask_details.name; // Le rendu final utilise le _render_ + props.internal_mask_details = result.render_mask_details; + props.internal_preview_mask_details = result.preview_mask_details; + props.internal_editor_mask_details = result.editor_mask_details; // On pourrait stocker celui-ci aussi + props.mask_last_update = Date.now(); + this.node.updatePropertiesJSON(); + + // 5. On met à jour les previews dans l'interface + const finalMaskUrl = new URL('/view', window.location.origin); + finalMaskUrl.searchParams.set("filename", result.preview_mask_details.name); + finalMaskUrl.searchParams.set("type", "input"); + finalMaskUrl.searchParams.set("t", Date.now()); + + const finalMaskImage = new Image(); + finalMaskImage.src = finalMaskUrl.href; + await new Promise(r => finalMaskImage.onload = r); + + this.node.loaded_preview_images[activeLayer.name.replace("layer_", "mask_")] = finalMaskImage; + + this.hide(); + this.node.toolbar.activeTool = 'mask'; + this.node.toolbar.maskManager.show(); + this.node.redrawPreviewCanvas(); + } + } catch (error) { + console.error("Erreur lors de l'application du masque:", error); + } finally { + applyButton.innerText = "Apply mask"; + applyButton.disabled = false; + } + }; + + // On assemble le tout + this.contextualToolbar.append( + toleranceLabel, toleranceInput, toleranceValue, + contiguousLabel, contiguousInput, + modeLabel, modeSelect, // <-- On l'ajoute ici + applyButton + ); + document.body.appendChild(this.contextualToolbar); + } + + // Affiche et positionne la barre d'outils + show() { + if (!this.contextualToolbar) return; + this.contextualToolbar.style.display = 'flex'; + this.positionToolbar(); + } + + // Cache la barre d'outils + hide() { + if (!this.contextualToolbar) return; + this.contextualToolbar.style.display = 'none'; + this.hideSelectionPreview(); // On efface le surlignage quand on change d'outil +} + + // Calcule la position idéale de la barre d'outils (en haut de la preview) + positionToolbar() { + if (!this.node.previewCanvas) return; + const canvasRect = this.node.previewCanvas.getBoundingClientRect(); + const toolbarRect = this.contextualToolbar.getBoundingClientRect(); + const left = canvasRect.left + (canvasRect.width / 2) - (toolbarRect.width / 2); + const top = canvasRect.top - toolbarRect.height - 2; + this.contextualToolbar.style.left = `${left}px`; + this.contextualToolbar.style.top = `${top}px`; + } +} \ No newline at end of file diff --git a/js/mask_manager.js b/js/mask_manager.js index 9d15e0f..d2a4cf8 100644 --- a/js/mask_manager.js +++ b/js/mask_manager.js @@ -73,16 +73,6 @@ async getSourceImageForActiveLayer() { } } - getActiveLayer() { - if (!this.node.layer_properties) return null; - const openLayerName = Object.keys(this.node.layer_properties).find(name => this.node.layer_properties[name]?.layer_collapsed === false); - if (openLayerName) { - const layerIndex = parseInt(openLayerName.split("_")[1]); - return { name: openLayerName, index: layerIndex }; - } - return null; - } - hasActiveMask() { if (!this.activeLayer) { return false; @@ -92,7 +82,7 @@ hasActiveMask() { } async handleCreateMask() { - this.activeLayer = this.getActiveLayer(); + this.activeLayer = this.node.getActiveLayer(); if (!this.activeLayer) return alert("Erreur : No active layer."); try { const sourceImage = await this.getSourceImageForActiveLayer(); @@ -116,7 +106,7 @@ hasActiveMask() { } async handleEditMask() { - this.activeLayer = this.getActiveLayer(); + this.activeLayer = this.node.getActiveLayer(); if (!this.activeLayer) return alert("Erreur : No active layer."); const layerProps = this.node.layer_properties[this.activeLayer.name]; @@ -228,7 +218,7 @@ async handleEditMask() { } } async handleMaskEditorClose(activeLayer) { - if (!activeLayer) activeLayer = this.getActiveLayer(); + if (!activeLayer) activeLayer = this.node.getActiveLayer(); if (!activeLayer) return; const returned_image = this.returned_image; if (!returned_image) return; @@ -273,7 +263,7 @@ async handleEditMask() { } handleDeleteMask() { - this.activeLayer = this.getActiveLayer(); + this.activeLayer = this.node.getActiveLayer(); if (!this.activeLayer) return; const layerProps = this.node.layer_properties[this.activeLayer.name]; if (layerProps && layerProps.internal_mask_filename) { @@ -376,7 +366,7 @@ createContextualToolbar() { } show() { - this.activeLayer = this.getActiveLayer(); + this.activeLayer = this.node.getActiveLayer(); if (!this.activeLayer) { this.hide(); return; } this.updateToolbarState(); const layerProps = this.node.layer_properties[this.activeLayer.name]; diff --git a/js/removebg.js b/js/removebg.js index 7fa4fb3..1887fee 100644 --- a/js/removebg.js +++ b/js/removebg.js @@ -6,7 +6,7 @@ export class RemoveBgManager { } async performRemoveBg() { - const activeLayer = this.maskManager.getActiveLayer(); + const activeLayer = this.node.getActiveLayer(); if (!activeLayer) return; const layerIndex = activeLayer.index; const layerName = activeLayer.name; diff --git a/js/toolbar.js b/js/toolbar.js index 2159edb..907575b 100644 --- a/js/toolbar.js +++ b/js/toolbar.js @@ -1,5 +1,6 @@ import { MaskManager } from './mask_manager.js'; import { RemoveBgManager } from './removebg.js'; +import { MagicWandManager } from './magic_wand_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"); @@ -68,8 +69,10 @@ export class Toolbar { this.clickTimeout = null; this.tools = [ { name: 'text', icon: textIconPath, y: 9 }, - { name: 'mask', icon: '🎭', y: 45 } + { name: 'mask', icon: '🎭', y: 45 }, + { name: 'magic_wand', icon: '🪄', y: 81 } ]; + this.toolBounds = {}; this.selectedTextObject = null; this.textEditTool = 'move'; @@ -86,9 +89,12 @@ export class Toolbar { this.maskManager = new MaskManager(this.node); this.removeBgManager = new RemoveBgManager(this.node, this.maskManager); + this.magicWandManager = new MagicWandManager(this.node); this.setupColorPicker(); this.createContextualToolbar(); + this.selectionSubMenu = null; // Notre futur menu de sélection + this.createSelectionSubMenu(); } createContextualToolbar() { ensureToolbarStyles(); @@ -204,6 +210,16 @@ createContextualToolbar() { y: iconY_start, size: iconSize }; + + const isActive = this.activeTool === tool.name || + (tool.name === 'magic_wand' && this.selectionSubMenu?.style.display === 'flex'); + + if (isActive) { + ctx.fillStyle = "rgba(255, 221, 255, 0.6)"; // Couleur de surbrillance + const padding = 2; + ctx.fillRect(iconX_start - padding, iconY_start - padding, iconSize + (padding * 2), iconSize + (padding * 2)); + } + ctx.save(); if (tool.icon instanceof Path2D) { ctx.translate(iconX_start, iconY_start); @@ -216,11 +232,11 @@ createContextualToolbar() { ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - if (this.activeTool === tool.name) { + /*if (this.activeTool === tool.name) { ctx.fillStyle = "rgba(255, 221, 255, 0.6)"; const padding = 2; ctx.fillRect(iconX_start - padding, iconY_start - padding, iconSize + (padding*2), iconSize + (padding*2)); - } + }*/ ctx.fillText(tool.icon, iconX_start + iconSize / 2, iconY_start + iconSize / 2); } @@ -258,32 +274,67 @@ createContextualToolbar() { }); ctx.restore(); } - handleClick(e, mouseX, mouseY) { - const clickedTool = this.tools.find(tool => { - const bounds = this.toolBounds[tool.name]; - if (!bounds) return false; - return mouseX >= bounds.x && mouseX < (bounds.x + bounds.size) && - mouseY >= bounds.y && mouseY < (bounds.y + bounds.size); - }); - if (clickedTool) { - const toolName = clickedTool.name; - const isCurrentlyActive = this.activeTool === toolName; - this.activeTool = null; - this.hideContextualToolbar(); - this.maskManager.hide(); - if (!isCurrentlyActive) { - this.activeTool = toolName; - } - if (this.activeTool === 'mask') { - this.maskManager.show(); - } - if (this.activeTool && this.node.movingLayer) { - this.node.movingLayer = null; - } - - this.node.refreshUI(); + +handleClick(e, mouseX, mouseY) { + const clickedTool = this.tools.find(tool => { + const bounds = this.toolBounds[tool.name]; + if (!bounds) return false; + return mouseX >= bounds.x && mouseX < (bounds.x + bounds.size) && + mouseY >= bounds.y && mouseY < (bounds.y + bounds.size); + }); + + if (clickedTool) { + const toolName = clickedTool.name; + //const isDeactivating = this.activeTool === toolName; + const isDeactivating = this.activeTool === toolName || + (toolName === 'magic_wand' && this.selectionSubMenu?.style.display === 'flex'); + + // --- Étape 1 : On nettoie systématiquement l'état précédent --- + this.activeTool = null; + this.maskManager.hide(); + this.magicWandManager.hide(); + if (this.selectionSubMenu) { + this.selectionSubMenu.style.display = 'none'; } + // --- Étape 2 : On active le nouvel outil, SEULEMENT si ce n'était pas un clic pour désactiver --- + if (!isDeactivating) { + if (toolName !== 'magic_wand') { + this.activeTool = toolName; + } + + + // On affiche l'interface correspondante + switch (toolName) { + case 'mask': + this.maskManager.show(); + break; + case 'magic_wand': + // Pour l'icône 🪄, "activer" signifie montrer le sous-menu + if (this.selectionSubMenu) { + this.selectionSubMenu.style.display = 'flex'; + // On positionne le sous-menu + this.positionSelectionSubMenu(); + //const bounds = this.toolBounds[toolName]; + //const canvasRect = this.node.previewCanvas.getBoundingClientRect(); + //this.selectionSubMenu.style.left = `${canvasRect.left + bounds.x + bounds.size}px`; + //this.selectionSubMenu.style.top = `${canvasRect.top + bounds.y}px`; + } + + break; + case 'text': + // L'outil texte est juste actif, il n'a pas d'interface à afficher via cette fonction + break; + } + } +if (clickedTool || this.activeTool) { + this.node.movingLayer = null; + +} + //this.node.graph.setDirtyCanvas(true, true); + this.node.refreshUI(); } +} + handleCanvasClick(e) { if (this.activeTool !== 'text') { return; @@ -454,6 +505,85 @@ handleDragEnd(e) { this.updateContextualToolbarPosition(); } } + +createSelectionSubMenu() { + this.selectionSubMenu = document.createElement("div"); + Object.assign(this.selectionSubMenu.style, { + position: 'fixed', + display: 'none', + zIndex: '10002', + backgroundColor: 'rgba(30, 30, 30, 0.9)', + border: '1px solid #555', + borderRadius: '8px', + padding: '4px', + //display: 'flex', + flexDirection: 'column', + gap: '4px', + }); + + // Petite fonction pour créer nos boutons-emojis + const createEmojiButton = (emoji, title) => { + const button = document.createElement("button"); + button.innerText = emoji; + button.title = title; + Object.assign(button.style, { + backgroundColor: '#444', + color: 'white', + border: '1px solid #666', + padding: '0', + borderRadius: '5px', + cursor: 'pointer', + width: '26px', + height: '26px', + fontSize: '16px', // Taille de l'emoji + textAlign: 'center', + lineHeight: '26px', // Centre l'emoji verticalement + }); + button.onmouseover = () => button.style.backgroundColor = '#5E5E5E'; + button.onmouseout = () => button.style.backgroundColor = '#444'; + return button; + }; + + // Bouton pour la Baguette Magique + const wandButton = createEmojiButton("🪄", "Baguette Magique"); + wandButton.onclick = () => { + this.activeTool = 'magic_wand'; + this.magicWandManager.show(); + this.selectionSubMenu.style.display = 'none'; + //if (this.node.movingLayer) this.node.movingLayer = null; + //this.node.refreshUI(); + }; + + // Bouton pour la Sélection Rectangle + 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'; + + }; + + this.selectionSubMenu.append(wandButton, rectButton); + document.body.appendChild(this.selectionSubMenu); +} + +positionSelectionSubMenu() { + // On s'assure que le menu existe et est visible avant de faire quoi que ce soit + if (!this.selectionSubMenu || this.selectionSubMenu.style.display === 'none') { + return; + } + + // On récupère la position de l'icône 🪄 sur laquelle on a cliqué + const bounds = this.toolBounds['magic_wand']; + if (!bounds || !this.node.previewCanvas) return; + + const canvasRect = this.node.previewCanvas.getBoundingClientRect(); + + // On positionne le sous-menu à côté de l'icône + this.selectionSubMenu.style.left = `${canvasRect.left + bounds.x + bounds.size}px`; + this.selectionSubMenu.style.top = `${canvasRect.top + bounds.y - 40}px`; +} + hideContextualToolbar() { this.selectedTextObject = null; this.contextualToolbar.style.display = 'none';