Add magic selection

This commit is contained in:
tritant
2025-09-19 18:23:00 +02:00
committed by GitHub
parent 74fe7fbdd4
commit 729a026e38
5 changed files with 565 additions and 49 deletions
+56 -6
View File
@@ -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 = [];
+346
View File
@@ -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 = `
<option value="add">➕ Add</option>
<option value="subtract">➖ Subtract</option>
<option value="intersect">✂️ Intersection</option>
`;
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`;
}
}
+5 -15
View File
@@ -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];
+1 -1
View File
@@ -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;
+157 -27
View File
@@ -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';