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`; } }