added free text
This commit is contained in:
@@ -19,6 +19,9 @@ $(document).ready(function () {
|
|||||||
let descriptionTextbox = null;
|
let descriptionTextbox = null;
|
||||||
let markerObjects = {};
|
let markerObjects = {};
|
||||||
let nextMarkerId = 1;
|
let nextMarkerId = 1;
|
||||||
|
let freeTextMode = false;
|
||||||
|
let freeTextObjects = {};
|
||||||
|
let nextFreeTextId = 1;
|
||||||
let partsListData = [];
|
let partsListData = [];
|
||||||
// DIMENSIONE GLOBALE MARKER
|
// DIMENSIONE GLOBALE MARKER
|
||||||
let globalMarkerSize = 16;
|
let globalMarkerSize = 16;
|
||||||
@@ -144,6 +147,9 @@ $(document).ready(function () {
|
|||||||
selectedPartNumber = null;
|
selectedPartNumber = null;
|
||||||
unsavedChanges = false;
|
unsavedChanges = false;
|
||||||
partsListData = [];
|
partsListData = [];
|
||||||
|
freeTextMode = false; // ← aggiungi
|
||||||
|
freeTextObjects = {}; // ← aggiungi
|
||||||
|
$("#addFreeTextBtnAnnotations").removeClass("active"); // ← aggiungi
|
||||||
if (fabricCanvas) {
|
if (fabricCanvas) {
|
||||||
fabricCanvas.off();
|
fabricCanvas.off();
|
||||||
fabricCanvas.dispose();
|
fabricCanvas.dispose();
|
||||||
@@ -192,6 +198,29 @@ $(document).ready(function () {
|
|||||||
updateMarkers();
|
updateMarkers();
|
||||||
markUnsaved();
|
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
|
// COLORE LISTA DESCRIZIONI
|
||||||
// ===================
|
// ===================
|
||||||
@@ -205,9 +234,22 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
if (descriptionTextbox && fabricCanvas) {
|
if (descriptionTextbox && fabricCanvas) {
|
||||||
descriptionTextbox.set("fill", globalDescriptionColor);
|
descriptionTextbox.set("fill", globalDescriptionColor);
|
||||||
fabricCanvas.renderAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
markUnsaved();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -382,7 +424,29 @@ $(document).ready(function () {
|
|||||||
console.log(
|
console.log(
|
||||||
"Evento mouse:down su canvas, selectedPartNumber:",
|
"Evento mouse:down su canvas, selectedPartNumber:",
|
||||||
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) {
|
if (selectedPartNumber === null) {
|
||||||
console.log(
|
console.log(
|
||||||
"Nessuna parte selezionata, ignoro il click.",
|
"Nessuna parte selezionata, ignoro il click.",
|
||||||
@@ -437,6 +501,7 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
updateDescriptions();
|
updateDescriptions();
|
||||||
|
updateFreeTexts();
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -997,7 +1062,128 @@ $(document).ready(function () {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// ===================
|
||||||
|
// 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
|
// MARKERS & DESCRIPTIONS
|
||||||
// ===================
|
// ===================
|
||||||
@@ -1303,6 +1489,40 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// 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"
|
// Delegazione evento per il pulsante "Descrizioni"
|
||||||
$(document)
|
$(document)
|
||||||
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
|
.off("click.addDescriptions", "#addDescriptionsBtnAnnotations")
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
|
style="width: 34px; height: 28px; padding: 1px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
|
<button type="button" class="btn btn-primary btn-sm" id="addDescriptionsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Aggiungi Lista Descrizioni</button>
|
||||||
|
<button type="button" class="btn btn-outline-dark btn-sm" id="addFreeTextBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-font"></i> Testo Libero</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm" id="removeAnnotationsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rimuovi Descrizioni</button>
|
<button type="button" class="btn btn-danger btn-sm" id="removeAnnotationsBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Rimuovi Descrizioni</button>
|
||||||
<button type="button" class="btn btn-warning btn-sm" id="undoMarkerBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Undo Marker</button>
|
<button type="button" class="btn btn-warning btn-sm" id="undoMarkerBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Undo Marker</button>
|
||||||
<button type="button" class="btn btn-success btn-sm" id="savePhotoBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Salva Foto con Nome</button>
|
<button type="button" class="btn btn-success btn-sm" id="savePhotoBtnAnnotations" style="padding: 0.1rem 0.5rem; font-size: 0.8rem;">Salva Foto con Nome</button>
|
||||||
@@ -280,5 +281,11 @@
|
|||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addFreeTextBtnAnnotations.active {
|
||||||
|
background-color: #343a40 !important;
|
||||||
|
border-color: #343a40 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
|
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
|
||||||
Reference in New Issue
Block a user