1843 lines
72 KiB
JavaScript
1843 lines
72 KiB
JavaScript
$(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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante. Impossibile inizializzare il modale delle annotazioni.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto trovata per questo elemento.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
}
|
|
} else {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">' +
|
|
(response.message ||
|
|
"Errore nel caricamento della foto.") +
|
|
"</div>",
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento della foto: ' +
|
|
error +
|
|
" (" +
|
|
xhr.status +
|
|
")</div>",
|
|
);
|
|
$("#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 = $(
|
|
'<select id="photoSelectorAnnotations" class="form-control"></select>',
|
|
);
|
|
photos.forEach((photo, index) => {
|
|
const photoName = photo.split("/").pop();
|
|
const option = $("<option></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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #downloadPhotoBtnAnnotations non trovato nel DOM.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da scaricare.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Impossibile caricare l\'immagine per il download.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Elemento #partsListAnnotations non trovato nel DOM.</div>',
|
|
);
|
|
$("#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) =>
|
|
`<div class="color-option" style="background-color: ${color}; width: 20px; height: 20px; display: inline-block; margin: 2px; cursor: pointer; pointer-events: auto;" data-color="${color}"></div>`,
|
|
)
|
|
.join("");
|
|
const listItem = `
|
|
<li class="list-group-item" data-part-number="${partNumber}">
|
|
<span class="part-info" style="cursor: pointer;">${partNumber} - ${partDescription}</span>
|
|
<div class="color-picker-container" style="display:inline-block; position: relative; overflow: visible; z-index: 2000; margin-left:5px;">
|
|
<div class="color-option selected-color" style="background-color: ${partColor}; width: 20px; height: 20px; display: inline-block; cursor: pointer; pointer-events: auto;"></div>
|
|
<div class="color-picker" style="display: none; position: absolute; right: 0; z-index: 2000; background: #fff; border: 1px solid #ccc; padding: 5px;">${colorOptions}</div>
|
|
</div>
|
|
<div style="display:inline-flex; align-items:center; gap:4px; margin-left:5px;">
|
|
<input type="range" class="marker-size-slider"
|
|
min="12" max="48" step="2"
|
|
value="${partSizes[partNumber] || globalMarkerSize}"
|
|
style="width:70px;">
|
|
<span class="marker-size-value" style="font-size:0.8rem;">${partSizes[partNumber] || globalMarkerSize}px</span>
|
|
</div>
|
|
</li>`;
|
|
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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Checkbox #showMixPartsAnnotations non trovato nel DOM.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: ID TRF mancante per il caricamento delle parti.</div>',
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">' +
|
|
(response.message ||
|
|
"Nessuna parte trovata per questo elemento.") +
|
|
"</div>",
|
|
);
|
|
$("#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 = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel caricamento delle parti: ' +
|
|
error +
|
|
" (" +
|
|
xhr.status +
|
|
")</div>",
|
|
);
|
|
$("#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 <textarea> nascosta (usata per catturare la
|
|
// tastiera durante l'editing) a document.body, FUORI dal modale.
|
|
// Il focus trap di Bootstrap la considera "esterna" e ruba il focus
|
|
// ogni volta che Fabric lo assegna a quella textarea — così i tasti
|
|
// digitati non arrivano mai al testo. Spostiamo la textarea dentro
|
|
// il modale non appena l'editing viene avviato.
|
|
itext.on("editing:entered", function () {
|
|
const modalEl = document.getElementById("annotationsModal");
|
|
if (
|
|
this.hiddenTextarea &&
|
|
modalEl &&
|
|
this.hiddenTextarea.parentNode !== modalEl
|
|
) {
|
|
modalEl.appendChild(this.hiddenTextarea);
|
|
}
|
|
});
|
|
itext.on("moving", function () {
|
|
entry.x = this.left / photoData.scale;
|
|
entry.y = this.top / photoData.scale;
|
|
markUnsaved();
|
|
});
|
|
|
|
itext.on("editing:exited", function () {
|
|
entry.text = this.text;
|
|
markUnsaved();
|
|
});
|
|
|
|
itext.on("scaling", function () {
|
|
entry.fontSize = Math.round(this.fontSize * this.scaleY);
|
|
this.set({ fontSize: entry.fontSize, scaleX: 1, scaleY: 1 });
|
|
markUnsaved();
|
|
});
|
|
|
|
fabricCanvas.add(itext);
|
|
freeTextObjects[entry.id] = itext;
|
|
|
|
// IMPORTANTE: non attivare l'editing nello stesso evento mouse:down che
|
|
// ha creato l'oggetto. Fabric processa quel click come "click su canvas
|
|
// vuoto" (il target non esisteva ancora al momento del mousedown) e al
|
|
// successivo mouseup deseleziona tutto, interrompendo l'editing appena
|
|
// avviato — l'utente non riesce a scrivere né a spostare il testo.
|
|
// Rimandiamo l'attivazione al prossimo giro di event loop, così avviene
|
|
// DOPO che Fabric ha concluso la gestione nativa di quel click.
|
|
if (startEditing) {
|
|
setTimeout(() => {
|
|
fabricCanvas.setActiveObject(itext);
|
|
itext.enterEditing();
|
|
itext.selectAll();
|
|
fabricCanvas.renderAll();
|
|
}, 0);
|
|
}
|
|
|
|
fabricCanvas.renderAll();
|
|
}
|
|
function updateFreeTexts() {
|
|
for (let id in freeTextObjects) {
|
|
fabricCanvas.remove(freeTextObjects[id]);
|
|
delete freeTextObjects[id];
|
|
}
|
|
freeTextObjects = {};
|
|
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
const annotations = photoAnnotations[currentPhoto];
|
|
if (!annotations || !annotations.freeTexts) return;
|
|
|
|
annotations.freeTexts.forEach((entry) => renderFreeText(entry, false));
|
|
}
|
|
// ===================
|
|
// MARKERS & DESCRIPTIONS
|
|
// ===================
|
|
function updateMarkers() {
|
|
console.log(
|
|
"updateMarkers chiamato, markerObjects:",
|
|
Object.keys(markerObjects),
|
|
);
|
|
for (let markerId in markerObjects) {
|
|
fabricCanvas.remove(markerObjects[markerId]);
|
|
delete markerObjects[markerId];
|
|
}
|
|
markerObjects = {};
|
|
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
const annotations = photoAnnotations[currentPhoto] || {
|
|
markers: [],
|
|
hasDescriptions: false,
|
|
descriptionPosition: { x: 10, y: 10 },
|
|
descriptionSize: {
|
|
width: photoData.displayWidth * 0.3,
|
|
height: photoData.displayHeight * 0.3,
|
|
},
|
|
};
|
|
const showMixParts = $("#showMixPartsAnnotations").is(":checked");
|
|
|
|
annotations.markers.forEach((marker) => {
|
|
const part = partsListData.find(
|
|
(p) => p.part_number == marker.partNumber,
|
|
);
|
|
const partDescription = part ? part.part_description : "";
|
|
const isMixPart =
|
|
part && String(part.mix || "N").toUpperCase() === "Y";
|
|
|
|
if (!showMixParts && isMixPart) {
|
|
console.log("Ignoro marker per parte Mix:", marker.partNumber);
|
|
return;
|
|
}
|
|
|
|
const size = partSizes[marker.partNumber] || globalMarkerSize;
|
|
const radius = size / 2;
|
|
const fontSize = Math.max(10, Math.round(size * 0.65));
|
|
|
|
const markerColor =
|
|
marker.color || partColors[marker.partNumber] || "#ff0000";
|
|
|
|
const circle = new fabric.Circle({
|
|
radius: radius,
|
|
fill: markerColor,
|
|
stroke: markerColor,
|
|
strokeWidth: 1,
|
|
left: marker.x * photoData.scale,
|
|
top: marker.y * photoData.scale,
|
|
originX: "center",
|
|
originY: "center",
|
|
selectable: true,
|
|
hasControls: false,
|
|
lockScalingX: true,
|
|
lockScalingY: true,
|
|
lockRotation: true,
|
|
});
|
|
|
|
const text = new fabric.Text(marker.partNumber.toString(), {
|
|
fontFamily: "Arial",
|
|
fontSize: fontSize,
|
|
fontWeight: "bold",
|
|
fill: "#ffffff",
|
|
left: marker.x * photoData.scale,
|
|
top: marker.y * photoData.scale,
|
|
originX: "center",
|
|
originY: "middle",
|
|
selectable: true,
|
|
hasControls: false,
|
|
lockScalingX: true,
|
|
lockScalingY: true,
|
|
lockRotation: true,
|
|
});
|
|
|
|
const group = new fabric.Group([circle, text], {
|
|
left: marker.x * photoData.scale,
|
|
top: marker.y * photoData.scale,
|
|
originX: "center",
|
|
originY: "center",
|
|
selectable: true,
|
|
hasControls: false,
|
|
lockScalingX: true,
|
|
lockScalingY: true,
|
|
lockRotation: true,
|
|
});
|
|
|
|
group.markerId = marker.id;
|
|
group.partNumber = marker.partNumber;
|
|
|
|
group.on("moving", function () {
|
|
marker.x = this.left / photoData.scale;
|
|
marker.y = this.top / photoData.scale;
|
|
console.log("Marker spostato:", {
|
|
partNumber: marker.partNumber,
|
|
x: marker.x,
|
|
y: marker.y,
|
|
});
|
|
markUnsaved();
|
|
});
|
|
|
|
fabricCanvas.add(group);
|
|
markerObjects[marker.id] = group;
|
|
});
|
|
|
|
fabricCanvas.renderAll();
|
|
console.log(
|
|
"Canvas aggiornato, numero di marker:",
|
|
Object.keys(markerObjects).length,
|
|
);
|
|
}
|
|
|
|
function updateDescriptions() {
|
|
console.log("updateDescriptions chiamato");
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
const annotations = photoAnnotations[currentPhoto] || {
|
|
markers: [],
|
|
hasDescriptions: false,
|
|
descriptionPosition: { x: 10, y: 10 },
|
|
descriptionSize: {
|
|
width: photoData.displayWidth * 0.3,
|
|
height: photoData.displayHeight * 0.3,
|
|
},
|
|
};
|
|
const showMixParts = $("#showMixPartsAnnotations").is(":checked");
|
|
|
|
if (!annotations.hasDescriptions) {
|
|
if (descriptionTextbox) {
|
|
fabricCanvas.remove(descriptionTextbox);
|
|
descriptionTextbox = null;
|
|
fabricCanvas.renderAll();
|
|
}
|
|
console.log(
|
|
"Nessuna descrizione attiva, rimuovo textbox se presente.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const partsList = partsListData
|
|
.filter((part) => {
|
|
const isMixPart = String(part.mix || "N").toUpperCase() === "Y";
|
|
return showMixParts || !isMixPart;
|
|
})
|
|
.map((part) => `${part.part_number} ${part.part_description}`);
|
|
|
|
const text = partsList.join("\n");
|
|
console.log("Testo descrizione generato:", text);
|
|
|
|
if (descriptionTextbox) {
|
|
fabricCanvas.remove(descriptionTextbox);
|
|
descriptionTextbox = null;
|
|
}
|
|
|
|
descriptionTextbox = new fabric.Textbox(text, {
|
|
left: annotations.descriptionPosition.x * photoData.scale,
|
|
top: annotations.descriptionPosition.y * photoData.scale,
|
|
width: annotations.descriptionSize.width,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
backgroundColor: "transparent",
|
|
fontFamily: "Arial",
|
|
fontSize: Math.max(16, Math.round(globalMarkerSize * 0.8)),
|
|
fill: globalDescriptionColor,
|
|
padding: 10,
|
|
editable: false,
|
|
selectable: true,
|
|
hasControls: true,
|
|
borderColor: "#ccc",
|
|
cornerColor: "#888",
|
|
cornerSize: 10,
|
|
transparentCorners: false,
|
|
lockRotation: true,
|
|
lockScalingFlip: true,
|
|
minScaleLimit: 0.1,
|
|
});
|
|
|
|
descriptionTextbox.on("scaling", function () {
|
|
fitFontToBox(this);
|
|
annotations.descriptionSize.width = this.width * this.scaleX;
|
|
annotations.descriptionSize.height = this.height * this.scaleY;
|
|
console.log(
|
|
"Descrizione ridimensionata:",
|
|
annotations.descriptionSize,
|
|
);
|
|
markUnsaved();
|
|
});
|
|
|
|
descriptionTextbox.on("moving", function () {
|
|
annotations.descriptionPosition.x = this.left / photoData.scale;
|
|
annotations.descriptionPosition.y = this.top / photoData.scale;
|
|
console.log(
|
|
"Descrizione spostata:",
|
|
annotations.descriptionPosition,
|
|
);
|
|
markUnsaved();
|
|
});
|
|
|
|
fabricCanvas.add(descriptionTextbox);
|
|
fitFontToBox(descriptionTextbox);
|
|
fabricCanvas.renderAll();
|
|
console.log("Descrizione aggiunta al canvas:", text);
|
|
}
|
|
|
|
function fitFontToBox(textbox) {
|
|
let fontSize = 24;
|
|
textbox.set("fontSize", fontSize);
|
|
textbox.setCoords();
|
|
|
|
while (
|
|
(textbox.textLines.length * textbox.fontSize * 1.2 >
|
|
textbox.height * textbox.scaleY ||
|
|
textbox._getTransformedDimensions().x >
|
|
textbox.width * textbox.scaleX) &&
|
|
fontSize > 8
|
|
) {
|
|
fontSize -= 1;
|
|
textbox.set("fontSize", fontSize);
|
|
textbox.setCoords();
|
|
}
|
|
|
|
while (
|
|
textbox.textLines.length * textbox.fontSize * 1.2 <
|
|
textbox.height * textbox.scaleY &&
|
|
textbox._getTransformedDimensions().x <
|
|
textbox.width * textbox.scaleX &&
|
|
fontSize < 32
|
|
) {
|
|
fontSize += 1;
|
|
textbox.set("fontSize", fontSize);
|
|
textbox.setCoords();
|
|
}
|
|
}
|
|
|
|
function clearCanvasMarkers(clearDescriptions = true) {
|
|
console.log(
|
|
"clearCanvasMarkers chiamato, clearDescriptions:",
|
|
clearDescriptions,
|
|
);
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
if (clearDescriptions && photoAnnotations[currentPhoto]) {
|
|
photoAnnotations[currentPhoto].hasDescriptions = false;
|
|
photoAnnotations[currentPhoto].descriptionPosition = {
|
|
x: 10,
|
|
y: 10,
|
|
};
|
|
photoAnnotations[currentPhoto].descriptionSize = {
|
|
width: photoData.displayWidth * 0.3,
|
|
height: photoData.displayHeight * 0.3,
|
|
};
|
|
if (descriptionTextbox) {
|
|
fabricCanvas.remove(descriptionTextbox);
|
|
descriptionTextbox = null;
|
|
}
|
|
}
|
|
|
|
for (let markerId in markerObjects) {
|
|
fabricCanvas.remove(markerObjects[markerId]);
|
|
delete markerObjects[markerId];
|
|
}
|
|
markerObjects = {};
|
|
|
|
const canvas = document.getElementById("photoCanvasAnnotations");
|
|
const ctx = canvas.getContext("2d");
|
|
canvas.width = photoData.naturalWidth;
|
|
canvas.height = photoData.naturalHeight;
|
|
canvas.style.width = `${photoData.displayWidth}px`;
|
|
canvas.style.height = `${photoData.displayHeight}px`;
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
if ($("#samplePhotoAnnotations")[0].naturalWidth) {
|
|
ctx.drawImage(
|
|
$("#samplePhotoAnnotations")[0],
|
|
0,
|
|
0,
|
|
canvas.width,
|
|
canvas.height,
|
|
);
|
|
}
|
|
updateMarkers();
|
|
if (photoAnnotations[currentPhoto]?.hasDescriptions) {
|
|
updateDescriptions();
|
|
}
|
|
markUnsaved();
|
|
}
|
|
|
|
function undoLastMarker() {
|
|
console.log("undoLastMarker chiamato");
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
if (
|
|
photoAnnotations[currentPhoto] &&
|
|
photoAnnotations[currentPhoto].markers.length > 0
|
|
) {
|
|
const lastMarker = photoAnnotations[currentPhoto].markers.pop();
|
|
if (markerObjects[lastMarker.id]) {
|
|
fabricCanvas.remove(markerObjects[lastMarker.id]);
|
|
delete markerObjects[lastMarker.id];
|
|
fabricCanvas.renderAll();
|
|
}
|
|
console.log("Ultimo marker rimosso:", lastMarker);
|
|
markUnsaved();
|
|
}
|
|
}
|
|
|
|
// ===================
|
|
// TESTO LIBERO — RIMOZIONE CON CANC/DELETE
|
|
// ===================
|
|
$(document).on("keydown.freeTextDelete", function (e) {
|
|
// Solo se il modale annotazioni è aperto
|
|
if (!$("#annotationsModal").hasClass("show")) return;
|
|
if (!fabricCanvas) return;
|
|
|
|
// Ignora se si sta scrivendo dentro un campo di editing testo
|
|
// (es. l'utente sta modificando il contenuto dell'IText stesso)
|
|
const active = fabricCanvas.getActiveObject();
|
|
if (active && active.isEditing) return;
|
|
|
|
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
if (!active || active.freeTextId === undefined) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const currentPhoto = $("#samplePhotoAnnotations").attr("src");
|
|
const annotations = photoAnnotations[currentPhoto];
|
|
|
|
if (annotations && annotations.freeTexts) {
|
|
annotations.freeTexts = annotations.freeTexts.filter(
|
|
(entry) => entry.id !== active.freeTextId,
|
|
);
|
|
}
|
|
|
|
fabricCanvas.remove(active);
|
|
delete freeTextObjects[active.freeTextId];
|
|
fabricCanvas.renderAll();
|
|
|
|
markUnsaved();
|
|
});
|
|
|
|
// Delegazione evento per il pulsante "Descrizioni"
|
|
$(document)
|
|
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
|
|
.on(
|
|
"click.addDescriptions",
|
|
"#addDescriptionsBtnAnnotations",
|
|
function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(
|
|
"Evento click su #addDescriptionsBtnAnnotations, ID elemento:",
|
|
$(this).attr("id"),
|
|
);
|
|
if (!$("#addDescriptionsBtnAnnotations").length) {
|
|
console.error(
|
|
"Pulsante #addDescriptionsBtnAnnotations non trovato nel DOM.",
|
|
);
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #addDescriptionsBtnAnnotations non trovato nel DOM.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
photoAnnotations[currentPhoto].hasDescriptions = true;
|
|
updateDescriptions();
|
|
markUnsaved();
|
|
},
|
|
);
|
|
|
|
// Delegazione evento per il pulsante "Rimuovi"
|
|
$(document)
|
|
.off("click.removeAnnotations", "#removeAnnotationsBtnAnnotations")
|
|
.on(
|
|
"click.removeAnnotations",
|
|
"#removeAnnotationsBtnAnnotations",
|
|
function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(
|
|
"Evento click su #removeAnnotationsBtnAnnotations, ID elemento:",
|
|
$(this).attr("id"),
|
|
);
|
|
if (!$("#removeAnnotationsBtnAnnotations").length) {
|
|
console.error(
|
|
"Pulsante #removeAnnotationsBtnAnnotations non trovato nel DOM.",
|
|
);
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Pulsante #removeAnnotationsBtnAnnotations non trovato nel DOM.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
clearCanvasMarkers(true);
|
|
},
|
|
);
|
|
|
|
// Delegazione evento per il pulsante "Undo"
|
|
$(document)
|
|
.off("click.undoMarker", "#undoMarkerBtnAnnotations")
|
|
.on("click.undoMarker", "#undoMarkerBtnAnnotations", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(
|
|
"Evento click su #undoMarkerBtnAnnotations, ID elemento:",
|
|
$(this).attr("id"),
|
|
);
|
|
undoLastMarker();
|
|
});
|
|
|
|
// ===================
|
|
// SAVE PHOTO
|
|
// ===================
|
|
$(document)
|
|
.off("click.savePhoto", "#savePhotoBtnAnnotations")
|
|
.on("click.savePhoto", "#savePhotoBtnAnnotations", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
console.log(
|
|
"Evento click su #savePhotoBtnAnnotations, ID elemento:",
|
|
$(this).attr("id"),
|
|
);
|
|
if (!$("#samplePhotoAnnotations").attr("src")) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Nessuna foto caricata da salvare.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
|
|
if (!fabricCanvas) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: Canvas Fabric.js non inizializzato.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
|
|
const canvas = document.getElementById("photoCanvasAnnotations");
|
|
const ctx = canvas.getContext("2d");
|
|
const img = $("#samplePhotoAnnotations")[0];
|
|
|
|
if (!img.complete || img.naturalWidth === 0) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore: l\'immagine non è caricata correttamente.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
|
|
canvas.width = photoData.naturalWidth;
|
|
canvas.height = photoData.naturalHeight;
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
|
|
try {
|
|
const fabricDataURL = fabricCanvas.toDataURL({
|
|
format: "png",
|
|
multiplier: 1 / photoData.scale,
|
|
});
|
|
|
|
const tempImg = new Image();
|
|
tempImg.src = fabricDataURL;
|
|
tempImg.onload = function () {
|
|
ctx.drawImage(tempImg, 0, 0, canvas.width, canvas.height);
|
|
|
|
fetch(fabricDataURL)
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
canvas.toBlob(function (blob) {
|
|
if (!blob) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la creazione dell\'immagine finale.</div>',
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(
|
|
errorMsg,
|
|
);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
return;
|
|
}
|
|
|
|
const timestamp = new Date()
|
|
.toISOString()
|
|
.replace(/[:.]/g, "-");
|
|
const iddatadb =
|
|
$("#annotationsModal").data("iddatadb");
|
|
const idquotations =
|
|
$("#annotationsModal").data("idquotations");
|
|
const id = iddatadb || idquotations;
|
|
const endpoint = idquotations
|
|
? "save_annotated_photo_quotation.php"
|
|
: "save_annotated_photo.php";
|
|
const finalName = `photo_${id}_${timestamp}.png`;
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", blob, finalName);
|
|
formData.append("filename", finalName);
|
|
formData.append(
|
|
idquotations ? "idquotations" : "iddatadb",
|
|
id,
|
|
);
|
|
|
|
$.ajax({
|
|
url: endpoint,
|
|
method: "POST",
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
success: function (response) {
|
|
if (response.success) {
|
|
const successMsg = $(
|
|
'<div class="alert alert-success temp-alert" role="alert">Foto salvata con successo: ' +
|
|
response.file_path +
|
|
"</div>",
|
|
);
|
|
$(
|
|
"#annotationsModal .modal-body",
|
|
).prepend(successMsg);
|
|
setTimeout(function () {
|
|
successMsg.fadeOut(
|
|
500,
|
|
function () {
|
|
$(this).remove();
|
|
},
|
|
);
|
|
}, 5000);
|
|
|
|
const photoSelector = $(
|
|
"#photoSelectorAnnotations",
|
|
);
|
|
if (photoSelector.length > 0) {
|
|
const newPhotoPath =
|
|
response.file_path;
|
|
const newPhotoName =
|
|
newPhotoPath
|
|
.split("/")
|
|
.pop();
|
|
const optionCount =
|
|
photoSelector.find(
|
|
"option",
|
|
).length;
|
|
|
|
const newOption = $(
|
|
"<option></option>",
|
|
)
|
|
.val(newPhotoPath)
|
|
.text(
|
|
`Photo ${optionCount + 1} - ${newPhotoName}`,
|
|
);
|
|
photoSelector.append(newOption);
|
|
photoSelector
|
|
.val(newPhotoPath)
|
|
.trigger("change");
|
|
}
|
|
|
|
unsavedChanges = false;
|
|
} else {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della foto: ' +
|
|
response.message +
|
|
"</div>",
|
|
);
|
|
$(
|
|
"#annotationsModal .modal-body",
|
|
).prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(
|
|
500,
|
|
function () {
|
|
$(this).remove();
|
|
},
|
|
);
|
|
}, 5000);
|
|
}
|
|
},
|
|
error: function (xhr, status, error) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore nel salvataggio della foto: ' +
|
|
error +
|
|
" (" +
|
|
xhr.status +
|
|
")</div>",
|
|
);
|
|
$(
|
|
"#annotationsModal .modal-body",
|
|
).prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
},
|
|
});
|
|
}, "image/png");
|
|
});
|
|
};
|
|
} catch (e) {
|
|
const errorMsg = $(
|
|
'<div class="alert alert-danger temp-alert" role="alert">Errore durante la generazione dell\'immagine: ' +
|
|
e.message +
|
|
"</div>",
|
|
);
|
|
$("#annotationsModal .modal-body").prepend(errorMsg);
|
|
setTimeout(function () {
|
|
errorMsg.fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}, 5000);
|
|
}
|
|
});
|
|
|
|
function markUnsaved() {
|
|
if (!unsavedChanges) {
|
|
unsavedChanges = true;
|
|
console.log("Modifiche non salvate rilevate.");
|
|
}
|
|
}
|
|
});
|