export class MagicWandManager {
constructor(node) {
this.node = node;
this.contextualToolbar = null;
this.activeMaskPreview = null;
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;
console.log("--- DIAGNOSTIC BAGUETTE MAGIQUE ---");
console.log(`clickX (e.offsetX): ${clickX}`);
console.log(`preview.width: ${preview.width}`);
console.log(`toolbar.width: ${toolbar.width}`);
console.log(`baseImage.naturalWidth: ${this.node.basePreviewImage.naturalWidth}`);
console.log(`Variable this.node.previewCanvasScale: ${this.node.previewCanvasScale}`);
console.log(`props.scale: ${props.scale}`);
console.log(`props.offset_x: ${props.offset_x}`);
console.log("---------------------------------");
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);
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) {
highlightData.data[i] = 255;
highlightData.data[i + 1] = 0;
highlightData.data[i + 2] = 0;
highlightData.data[i + 3] = 102;
}
}
tempCtx.putImageData(highlightData, 0, 0);
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);
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',
left: '20px',
zIndex: '10001',
backgroundColor: 'rgba(40, 40, 40, 0.9)',
border: '1px solid #555',
borderRadius: '8px',
padding: '10px',
alignItems: 'center',
gap: '6px',
fontFamily: 'sans-serif',
fontSize: '14px',
color: 'white',
});
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";
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;
};
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 {
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);
const props = this.node.layer_properties[activeLayer.name];
const existingMaskFilename = props.internal_mask_filename || null;
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) {
props.internal_mask_filename = result.render_mask_details.name;
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;
props.mask_last_update = Date.now();
this.node.updatePropertiesJSON();
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;
}
};
this.contextualToolbar.append(
toleranceLabel, toleranceInput, toleranceValue,
contiguousLabel, contiguousInput,
modeLabel, modeSelect,
applyButton
);
document.body.appendChild(this.contextualToolbar);
}
show() {
if (!this.contextualToolbar) return;
this.contextualToolbar.style.display = 'flex';
this.positionToolbar();
}
hide() {
if (!this.contextualToolbar) return;
this.contextualToolbar.style.display = 'none';
this.hideSelectionPreview();
}
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`;
}
}