$(document).ready(function () { // =================== // GLOBAL STATE // =================== let photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; let photoAnnotations = {}; let partColors = {}; let partSizes = {}; // memorizza la dimensione specifica per parte let selectedPartNumber = null; let unsavedChanges = false; let fabricCanvas = null; let descriptionTextbox = null; let markerObjects = {}; let nextMarkerId = 1; let freeTextMode = false; let freeTextObjects = {}; let nextFreeTextId = 1; let partsListData = []; // DIMENSIONE GLOBALE MARKER let globalMarkerSize = 16; // COLORE TESTO LISTA DESCRIZIONI let globalDescriptionColor = "#000000"; // =================== // MODAL INITIALIZATION // =================== window.initAnnotationsModal = function (iddatadb, idquotations, trfHeader) { console.log("initAnnotationsModal chiamato con:", { iddatadb, idquotations, trfHeader, }); $("#annotationsModal").attr("data-iddatadb", iddatadb); if (!iddatadb && !idquotations) { const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } $("#trfHeaderAnnotations").text(trfHeader || "N/D"); $("#annotationsModal") .data("iddatadb", iddatadb || null) .data("idquotations", idquotations || null); // Parti (non legate al canvas) possono partire subito loadExistingParts(iddatadb, idquotations); const modalElement = document.getElementById("annotationsModal"); if (!modalElement) { console.error("Elemento #annotationsModal non trovato nel DOM."); alert( "Errore: Il modale delle annotazioni non è presente nel DOM.", ); return; } let modal = bootstrap.Modal.getInstance(modalElement); if (!modal) { modal = new bootstrap.Modal(modalElement, { backdrop: true, keyboard: true, focus: true, }); } // IMPORTANTE: carica la foto SOLO dopo che il modale è completamente // visibile (animazione fade terminata). Se l'immagine è già in cache // del browser, l'evento "load" può scattare quasi subito, PRIMA che // il contenitore del canvas abbia dimensioni reali (parent ancora // display:none durante il fade) — questo rompeva il canvas Fabric.js // per la prima foto mostrata (bug riprodotto solo con foto già in cache, // tipico del server di test dove le foto erano già state visualizzate). $(modalElement).one("shown.bs.modal", function () { loadPhoto(iddatadb, idquotations); }); modal.show(); // Inizializza slider dimensione marker $("#markerSizeSlider").val(globalMarkerSize); $("#markerSizeValue").text(globalMarkerSize + "px"); // Debug: Verifica presenza elementi DOM console.log( "Presenza #partsListAnnotations:", $("#partsListAnnotations").length, ); console.log( "Presenza #showMixPartsAnnotations:", $("#showMixPartsAnnotations").length, ); console.log( "Presenza #addDescriptionsBtnAnnotations:", $("#addDescriptionsBtnAnnotations").length, ); console.log( "Presenza #removeAnnotationsBtnAnnotations:", $("#removeAnnotationsBtnAnnotations").length, ); console.log( "Presenza #downloadPhotoBtnAnnotations:", $("#downloadPhotoBtnAnnotations").length, ); console.log( "Presenza #backToPartsBtnAnnotations:", $("#backToPartsBtnAnnotations").length, ); console.log( "Presenza #overlayCanvasAnnotations:", $("#overlayCanvasAnnotations").length, ); }; $("#annotationsModal").on("hide.bs.modal", function (e) { if ( unsavedChanges && !confirm("Hai modifiche non salvate. Vuoi davvero uscire?") ) { e.preventDefault(); } }); $("#annotationsModal").on("hidden.bs.modal", function () { photoData = { naturalWidth: 0, naturalHeight: 0, displayWidth: 0, displayHeight: 0, scale: 1, }; photoAnnotations = {}; partColors = {}; selectedPartNumber = null; unsavedChanges = false; partsListData = []; freeTextMode = false; // ← aggiungi freeTextObjects = {}; // ← aggiungi $("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi if (fabricCanvas) { fabricCanvas.off(); fabricCanvas.dispose(); fabricCanvas = null; } descriptionTextbox = null; markerObjects = {}; globalMarkerSize = 16; globalDescriptionColor = "#000000"; $("#photoSelectorContainerAnnotations").empty().hide(); $("#samplePhotoAnnotations").attr("src", ""); $("#partsListAnnotations").empty(); $(".temp-alert").remove(); const modalElement = document.getElementById("annotationsModal"); const modal = bootstrap.Modal.getInstance(modalElement); if (modal) { modal.dispose(); } $(".modal-backdrop").remove(); $("body").removeClass("modal-open"); $("body").css("padding-right", ""); $(":focus").blur(); }); // SLIDER DIMENSIONE MARKER $(document) .off("input", "#markerSizeSlider") .on("input", "#markerSizeSlider", function () { globalMarkerSize = parseInt($(this).val()); $("#markerSizeValue").text(globalMarkerSize + "px"); partsListData.forEach(function (part) { if (part && part.part_number != null) { partSizes[part.part_number] = globalMarkerSize; } }); $("#partsListAnnotations .marker-size-slider").val( globalMarkerSize, ); $("#partsListAnnotations .marker-size-value").text( globalMarkerSize + "px", ); updateMarkers(); markUnsaved(); }); // =================== // TESTO LIBERO — TOGGLE MODALITÀ // =================== $(document) .off("click.freeTextToggle", "#addFreeTextBtnAnnotations") .on("click.freeTextToggle", "#addFreeTextBtnAnnotations", function (e) { e.preventDefault(); e.stopPropagation(); freeTextMode = !freeTextMode; $(this).toggleClass("active", freeTextMode); // Disattiva la selezione parte/marker mentre si aggiunge testo libero if (freeTextMode) { selectedPartNumber = null; $("#partsListAnnotations .list-group-item").removeClass( "active", ); } console.log("Free text mode:", freeTextMode); }); // =================== // COLORE LISTA DESCRIZIONI // =================== $(document) .off("input.descriptionColor", "#descriptionColorPickerAnnotations") .on( "input.descriptionColor", "#descriptionColorPickerAnnotations", function () { globalDescriptionColor = $(this).val(); if (descriptionTextbox && fabricCanvas) { descriptionTextbox.set("fill", globalDescriptionColor); } // Aggiorna anche i testi liberi già piazzati sulla foto corrente const currentPhoto = $("#samplePhotoAnnotations").attr("src"); const annotations = photoAnnotations[currentPhoto]; if (annotations && annotations.freeTexts) { annotations.freeTexts.forEach((entry) => { entry.color = globalDescriptionColor; }); } Object.values(freeTextObjects).forEach((obj) => { obj.set("fill", globalDescriptionColor); }); if (fabricCanvas) fabricCanvas.renderAll(); markUnsaved(); }, ); // =================== // PHOTO LOADERS // =================== function loadPhoto(iddatadb, idquotations) { const currentPhoto = $("#samplePhotoAnnotations").attr("src"); const endpoint = idquotations ? "load_photo_quotation.php" : "load_photo.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; $.ajax({ url: endpoint, method: "GET", data: data, success: function (response) { console.log("Risposta da load_photo:", response); if (response.success) { if (response.photos && response.photos.length > 1) { showPhotoSelector(response.photos, currentPhoto); } else if ( response.photos && response.photos.length === 1 ) { loadSinglePhoto(response.photos[0]); } else { $("#samplePhotoAnnotations").attr("src", ""); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } } else { const errorMsg = $( '", ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { console.error("Errore AJAX in loadPhoto:", { status, error, responseText: xhr.responseText, }); const errorMsg = $( '", ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } function showPhotoSelector(photos, selected = null) { const selectorContainer = $("#photoSelectorContainerAnnotations"); selectorContainer.empty().show(); const selector = $( '', ); photos.forEach((photo, index) => { const photoName = photo.split("/").pop(); const option = $("") .val(photo) .text(`Photo ${index + 1} - ${photoName}`); selector.append(option); }); selector.on("change", function () { loadSinglePhoto($(this).val()); }); selectorContainer.append(selector); const photoToSelect = selected && photos.includes(selected) ? selected : photos[0]; if (photoToSelect) { selector.val(photoToSelect); loadSinglePhoto(photoToSelect); } } function loadSinglePhoto(photoPath) { const img = $("#samplePhotoAnnotations"); img.off("load").attr("src", photoPath); img.on("load", function () { console.log("Foto caricata:", photoPath); const canvas = document.getElementById("photoCanvasAnnotations"); const ctx = canvas.getContext("2d"); const naturalWidth = img[0].naturalWidth; const naturalHeight = img[0].naturalHeight; const parent = $(canvas).parent(); const maxW = parent.width(); const maxH = parent.height(); const scale = Math.min(maxW / naturalWidth, maxH / naturalHeight); photoData = { naturalWidth, naturalHeight, displayWidth: Math.max(1, Math.round(naturalWidth * scale)), displayHeight: Math.max(1, Math.round(naturalHeight * scale)), scale, }; canvas.width = naturalWidth; canvas.height = naturalHeight; canvas.style.width = `${photoData.displayWidth}px`; canvas.style.height = `${photoData.displayHeight}px`; ctx.clearRect(0, 0, naturalWidth, naturalHeight); ctx.drawImage(img[0], 0, 0, naturalWidth, naturalHeight); if (fabricCanvas) { fabricCanvas.off(); fabricCanvas.dispose(); fabricCanvas = null; } const overlayCanvas = document.getElementById( "overlayCanvasAnnotations", ); const canvasContainer = overlayCanvas.parentElement; const newOverlayCanvas = document.createElement("canvas"); newOverlayCanvas.id = "overlayCanvasAnnotations"; newOverlayCanvas.width = photoData.displayWidth; newOverlayCanvas.height = photoData.displayHeight; newOverlayCanvas.style.width = `${photoData.displayWidth}px`; newOverlayCanvas.style.height = `${photoData.displayHeight}px`; newOverlayCanvas.style.position = "absolute"; newOverlayCanvas.style.top = "0"; newOverlayCanvas.style.left = "0"; newOverlayCanvas.style.zIndex = "1000"; canvasContainer.removeChild(overlayCanvas); canvasContainer.appendChild(newOverlayCanvas); setTimeout(() => { fabricCanvas = new fabric.Canvas("overlayCanvasAnnotations", { selection: true, preserveObjectStacking: true, width: photoData.displayWidth, height: photoData.displayHeight, }); fabricCanvas.setDimensions({ width: photoData.displayWidth, height: photoData.displayHeight, }); fabricCanvas.on("mouse:down", function (options) { console.log( "Evento mouse:down su canvas, selectedPartNumber:", selectedPartNumber, "freeTextMode:", freeTextMode, ); // Modalità testo libero: ha priorità, un solo click e si disattiva da sola if (freeTextMode) { if (options.target) { console.log( "Click su un oggetto esistente, ignoro.", ); return; } const pointer = fabricCanvas.getPointer(options.e); const x = pointer.x / photoData.scale; const y = pointer.y / photoData.scale; addFreeText(x, y); freeTextMode = false; $("#addFreeTextBtnAnnotations").removeClass("active"); return; } if (selectedPartNumber === null) { console.log( "Nessuna parte selezionata, ignoro il click.", ); return; } if (options.target) { console.log("Click su un oggetto esistente, ignoro."); return; } const pointer = fabricCanvas.getPointer(options.e); const x = pointer.x / photoData.scale; const y = pointer.y / photoData.scale; const currentPhoto = $("#samplePhotoAnnotations").attr( "src", ); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; } const partColor = partColors[selectedPartNumber] || "#ff0000"; photoAnnotations[currentPhoto].markers.push({ id: nextMarkerId++, partNumber: selectedPartNumber, x, y, color: partColor, }); console.log("Marker aggiunto/spostato:", { partNumber: selectedPartNumber, x, y, color: partColor, }); updateMarkers(); markUnsaved(); }); fabricCanvas.upperCanvasEl.focus(); fabricCanvas.renderAll(); updateMarkers(); updateDescriptions(); updateFreeTexts(); }, 10); }); } // =================== // DOWNLOAD PHOTO // =================== $(document) .off("click.downloadPhoto", "#downloadPhotoBtnAnnotations") .on( "click.downloadPhoto", "#downloadPhotoBtnAnnotations", function (e) { e.preventDefault(); e.stopPropagation(); console.log( "Evento click su #downloadPhotoBtnAnnotations, ID elemento:", $(this).attr("id"), ); if (!$("#downloadPhotoBtnAnnotations").length) { console.error( "Pulsante #downloadPhotoBtnAnnotations non trovato nel DOM.", ); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } const photoSrc = $("#samplePhotoAnnotations").attr("src"); console.log("URL immagine per il download:", photoSrc); if (!photoSrc) { console.error("Nessuna foto caricata da scaricare."); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } // Verifica se l'URL è valido const img = new Image(); img.src = photoSrc; img.onload = function () { const photoName = photoSrc.split("/").pop() || "downloaded_photo.png"; console.log("Nome file per il download:", photoName); const link = document.createElement("a"); link.href = photoSrc; link.download = photoName; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log("Download avviato per:", photoName); }; img.onerror = function () { console.error( "Errore: Impossibile caricare l'immagine per il download:", photoSrc, ); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }; }, ); // =================== // TORNA AL MODALE PARTI (modal_partsTable.php) // =================== $(document) .off("click.backToParts", "#backToPartsBtnAnnotations") .on("click.backToParts", "#backToPartsBtnAnnotations", function (e) { e.preventDefault(); e.stopPropagation(); if ( unsavedChanges && !confirm( "Hai modifiche non salvate. Vuoi davvero tornare al modale delle parti?", ) ) { return; } const iddatadb = $("#annotationsModal").data("iddatadb"); const idquotations = $("#annotationsModal").data("idquotations"); const trfHeader = $("#trfHeaderAnnotations").text(); // CHIUDI MODALE ANNOTAZIONI IN MODO SICURO const annotationsModalElement = document.getElementById("annotationsModal"); if (annotationsModalElement) { const modalInstance = bootstrap.Modal.getInstance( annotationsModalElement, ); if (modalInstance) { modalInstance.hide(); } else { $(annotationsModalElement).modal("hide"); // fallback jQuery } } console.log("Torno a modal_partsTable.php con:", { iddatadb, idquotations, trfHeader, }); // CARICA E APRI MODALE PARTI $.get( "modal_partsTable.php", { iddatadb: iddatadb || "", idquotations: idquotations || "", trfHeader: trfHeader, }, function (data) { // Rimuovi vecchio modale (con ID corretto) $("#partsTableModal").remove(); $(".modal-backdrop").remove(); // Aggiungi nuovo modale $("body").append(data); // Apri con Bootstrap 5 const partsModalElement = document.getElementById("partsTableModal"); if (partsModalElement) { const modal = new bootstrap.Modal(partsModalElement, { backdrop: true, keyboard: true, focus: true, }); modal.show(); } else { let iddatadb = $("#annotationsModal").attr("data-iddatadb"); $( "button.parts-btn[data-iddatadb='" + iddatadb + "']", ).trigger("click"); } }, ).fail(function (xhr) { console.error("Errore caricamento modale parti:", xhr); alert( "Errore 404: modal_partsTable.php non trovato o errore server.", ); }); }); // =================== // PARTS LIST // =================== function updatePartsList() { console.log( "updatePartsList chiamato con partsListData:", partsListData, ); const showMixParts = $("#showMixPartsAnnotations").is(":checked"); console.log("Stato showMixPartsAnnotations:", showMixParts); const partsListElement = $("#partsListAnnotations"); if (!partsListElement.length) { console.error( "Elemento #partsListAnnotations non trovato nel DOM.", ); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } partsListElement.empty(); const predefinedColors = [ "#ff0000", // Rosso "#0000ff", // Blu "#00ff00", // Verde "#ffff00", // Giallo "#ff00ff", // Magenta "#00ffff", // Ciano "#800080", // Viola "#ffa500", // Arancione ]; partsListData.forEach((part) => { const partNumber = part.part_number; const partDescription = part.part_description; const isMixPart = String(part.mix || "N").toUpperCase() === "Y"; const partColor = partColors[partNumber] || (isMixPart ? "#0000ff" : "#ff0000"); if (partNumber && partDescription && (showMixParts || !isMixPart)) { const colorOptions = predefinedColors .map( (color) => `
`, ) .join(""); const listItem = `
  • ${partNumber} - ${partDescription}
    ${partSizes[partNumber] || globalMarkerSize}px
  • `; partsListElement.append(listItem); } }); console.log( "Elementi aggiunti a #partsListAnnotations:", partsListElement.find("li").length, ); console.log("HTML di #partsListAnnotations:", partsListElement.html()); // Associa evento di selezione all'intera riga partsListElement .off("click.partsList") .on("click.partsList", ".list-group-item", function (e) { e.stopPropagation(); e.preventDefault(); // Ignora il click se è sulla paletta dei colori if ($(e.target).closest(".color-picker-container").length) { console.log( "Click sulla paletta dei colori, ignoro selezione parte.", ); return; } const $listItem = $(this); const partNumber = $listItem.data("part-number"); if ( selectedPartNumber == partNumber && $listItem.hasClass("active") ) { selectedPartNumber = null; $listItem.removeClass("active"); return; } selectedPartNumber = partNumber; $listItem.addClass("active").siblings().removeClass("active"); console.log( "Parte selezionata tramite riga:", selectedPartNumber, ); }); // Associa eventi alla paletta dei colori partsListElement .off("click.selectedColor") .on("click.selectedColor", ".selected-color", function (e) { e.stopPropagation(); e.preventDefault(); const $picker = $(this).siblings(".color-picker"); console.log( "Cliccato .selected-color, mostro/nascondo paletta:", $picker.is(":visible"), ); $(".color-picker").not($picker).hide(); $picker.toggle(); }); // === Gestione cambio colore === partsListElement .off("click.colorOption") .on("click.colorOption", ".color-option", function (e) { e.stopPropagation(); e.preventDefault(); const $this = $(this); const color = $this.data("color"); const $listItem = $this.closest("li"); const partNumber = $listItem.data("part-number"); console.log( "Cliccato .color-option, colore:", color, "per parte:", partNumber, ); // Salva il nuovo colore partColors[partNumber] = color; // Aggiorna il colore visivo nel selettore $listItem .find(".selected-color") .css("background-color", color); let currentPhoto = $("#samplePhotoAnnotations").attr("src"); let annotations = photoAnnotations[currentPhoto]; if (annotations) { annotations.markers.forEach(function (m) { if (m.partNumber == partNumber) { m.color = color; let group = markerObjects[m.id]; if (group) { let circle = group.item(0); circle.set("fill", color); circle.set("stroke", color); } } }); fabricCanvas.renderAll(); } // Chiudi la palette e aggiorna canvas $this.closest(".color-picker").hide(); updateMarkers(); markUnsaved(); }); // === Slider locale per dimensione marker === partsListElement .off("input.localMarkerSize") .on("input.localMarkerSize", ".marker-size-slider", function (e) { e.stopPropagation(); e.preventDefault(); // Riferimenti alla parte e valore scelto const $slider = $(this); const partNumber = $slider.closest("li").data("part-number"); const newSize = parseInt($slider.val()); // Aggiorna il valore visivo accanto allo slider $slider.siblings(".marker-size-value").text(newSize + "px"); // Memorizza la nuova dimensione per quella parte partSizes[partNumber] = newSize; // Aggiorna i marker sul canvas updateMarkers(); // Segnala modifiche non salvate markUnsaved(); console.log( `Dimensione marker aggiornata per parte ${partNumber}: ${newSize}px`, ); }); $(document) .off("click.colorPicker") .on("click.colorPicker", function (e) { if (!$(e.target).closest(".color-picker-container").length) { console.log("Cliccato fuori, nascondo tutte le palette."); $(".color-picker").hide(); } }); } function enableDragDropPartsList() { const list = $("#partsListAnnotations"); if (!list.length || typeof $.ui === "undefined" || !$.ui.sortable) { console.warn("jQuery UI o .sortable non disponibile. Ritento..."); setTimeout(enableDragDropPartsList, 100); return; } if (list.hasClass("ui-sortable")) { list.sortable("destroy"); // evita duplicati } list.sortable({ items: "li.list-group-item", placeholder: "list-group-item placeholder", axis: "y", containment: "parent", tolerance: "pointer", start: function (e, ui) { ui.item.addClass("dragging"); }, stop: function (e, ui) { ui.item.removeClass("dragging"); const newOrder = []; list.find("li").each(function () { const partNumber = $(this).data("part-number"); const part = partsListData.find( (p) => p.part_number == partNumber, ); if (part) newOrder.push(part); }); partsListData = newOrder; console.log( "Ordine parti aggiornato:", partsListData.map((p) => p.part_number), ); markUnsaved(); updateMarkers(); }, }); } // Delegazione evento per il checkbox $(document) .off("change.showMix", "#showMixPartsAnnotations") .on("change.showMix", "#showMixPartsAnnotations", function (e) { e.preventDefault(); e.stopPropagation(); console.log( "Evento change su #showMixPartsAnnotations, ID elemento:", $(this).attr("id"), ); const isChecked = $(this).is(":checked"); console.log( "Checkbox #showMixPartsAnnotations cambiato:", isChecked, ); if (!$("#showMixPartsAnnotations").length) { console.error( "Checkbox #showMixPartsAnnotations non trovato nel DOM.", ); const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } updatePartsList(); updateMarkers(); setTimeout(enableDragDropPartsList, 50); if ( photoAnnotations[$("#samplePhotoAnnotations").attr("src")] ?.hasDescriptions ) { updateDescriptions(); } }); // =================== // LOAD EXISTING PARTS // =================== function loadExistingParts(iddatadb, idquotations) { console.log("loadExistingParts chiamato con:", { iddatadb, idquotations, }); if (!iddatadb && !idquotations) { const errorMsg = $( '', ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); return; } const endpoint = idquotations ? "load_parts_quotation.php" : "load_parts.php"; const data = idquotations ? { idquotations: idquotations } : { iddatadb: iddatadb }; $.ajax({ url: endpoint, method: "GET", data: data, success: function (response) { console.log("Risposta da load_parts:", response); partsListData = []; if ( response.success && response.parts && response.parts.length > 0 ) { partsListData = response.parts; response.parts.forEach((part) => { const isMixPart = String(part.mix || "N").toUpperCase() === "Y"; const defaultColor = isMixPart ? "#0000ff" : "#ff0000"; partColors[part.part_number] = defaultColor; }); updatePartsList(); // Forza aggiornamento iniziale del checkbox setTimeout(() => { $("#showMixPartsAnnotations").trigger("change.showMix"); }, 100); } else { const errorMsg = $( '", ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); } }, error: function (xhr, status, error) { console.error("Errore AJAX in loadExistingParts:", { status, error, responseText: xhr.responseText, }); const errorMsg = $( '", ); $("#annotationsModal .modal-body").prepend(errorMsg); setTimeout(function () { errorMsg.fadeOut(500, function () { $(this).remove(); }); }, 5000); }, }); } // =================== // TESTO LIBERO — CREAZIONE E RENDER // =================== function addFreeText(x, y) { const currentPhoto = $("#samplePhotoAnnotations").attr("src"); if (!photoAnnotations[currentPhoto]) { photoAnnotations[currentPhoto] = { markers: [], freeTexts: [], hasDescriptions: false, descriptionPosition: { x: 10, y: 10 }, descriptionSize: { width: photoData.displayWidth * 0.3, height: photoData.displayHeight * 0.3, }, }; } if (!photoAnnotations[currentPhoto].freeTexts) { photoAnnotations[currentPhoto].freeTexts = []; } const entry = { id: nextFreeTextId++, x, y, text: "Testo", color: globalDescriptionColor, fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)), }; photoAnnotations[currentPhoto].freeTexts.push(entry); renderFreeText(entry, true); markUnsaved(); } function renderFreeText(entry, startEditing = false) { const itext = new fabric.IText(entry.text, { left: entry.x * photoData.scale, top: entry.y * photoData.scale, fontFamily: "Arial", fontSize: entry.fontSize, fill: entry.color, selectable: true, hasControls: true, editable: true, borderColor: "#333", cornerColor: "#000", cornerSize: 12, transparentCorners: false, cornerStyle: "rect", borderScaleFactor: 2, }); itext.freeTextId = entry.id; // FIX: Fabric.js appende la