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';