Add magic selection
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user