cad area punti mpodifica manuale

This commit is contained in:
2026-06-16 09:44:19 +02:00
parent 8bb23ee563
commit 4c09a0dcb4
+490 -24
View File
@@ -280,6 +280,15 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
font-size: 0.9rem;
font-weight: 600;
}
.manual-edit-help {
background: #f8fafc;
border: 1px dashed #cbd5e1;
color: #475569;
border-radius: 10px;
padding: 8px 12px;
font-size: 0.9rem;
}
</style>
</head>
@@ -467,10 +476,18 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
Chiudi esclusione
</button>
<button type="button" id="toolEditBtn" class="btn btn-outline-secondary">
Modifica punti
</button>
<button type="button" id="undoPointBtn" class="btn btn-outline-secondary">
↩️ Annulla punto
</button>
<button type="button" id="deleteSelectedPointBtn" class="btn btn-outline-danger">
Elimina punto
</button>
<button type="button" id="resetManualBtn" class="btn btn-outline-warning">
🔄 Reset
</button>
@@ -562,6 +579,14 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
let polygonPoints = [];
let currentHolePoints = [];
let holes = [];
let selectedEditPoint = null;
let isDraggingEditPoint = false;
let editDragMoved = false;
const POINT_HIT_RADIUS = 12;
const SEGMENT_HIT_RADIUS = 12;
let lastManualResult = null;
$(document).ready(function() {
@@ -957,6 +982,10 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
currentHolePoints = [];
holes = [];
selectedEditPoint = null;
isDraggingEditPoint = false;
editDragMoved = false;
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
@@ -1023,24 +1052,52 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
w: 0,
h: 0
};
return;
}
if (currentTool === 'edit') {
selectedEditPoint = findNearestEditablePoint(pos, POINT_HIT_RADIUS);
if (selectedEditPoint) {
isDraggingEditPoint = true;
editDragMoved = false;
updateSelectedPointPosition(pos);
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
} else {
redrawManualOverlay();
setManualStatus('Modifica punti: clicca un punto per selezionarlo, trascinalo per spostarlo, doppio click su un segmento per inserire un nuovo punto.');
}
return;
}
};
manualOverlayCanvas.onmousemove = function(e) {
if (currentTool !== 'roi' || !isDrawingRoi) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'roi' && isDrawingRoi) {
roiRect = {
x: Math.min(roiStartX, pos.x),
y: Math.min(roiStartY, pos.y),
w: Math.abs(pos.x - roiStartX),
h: Math.abs(pos.y - roiStartY)
};
redrawManualOverlay();
return;
}
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
roiRect = {
x: Math.min(roiStartX, pos.x),
y: Math.min(roiStartY, pos.y),
w: Math.abs(pos.x - roiStartX),
h: Math.abs(pos.y - roiStartY)
};
redrawManualOverlay();
if (currentTool === 'edit' && isDraggingEditPoint && selectedEditPoint) {
updateSelectedPointPosition(pos);
editDragMoved = true;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
return;
}
};
manualOverlayCanvas.onmouseup = function() {
@@ -1055,14 +1112,48 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
updateManualPreview();
if (roiRect) {
setManualStatus('ROI definita. Ora clicca “Calibra quota” e seleziona due punti su una quota nota.');
setManualStatus('ROI definita. Ora puoi fare “Zoom su ROI” oppure calibrare una quota nota.');
}
return;
}
if (currentTool === 'edit' && isDraggingEditPoint) {
isDraggingEditPoint = false;
if (selectedEditPoint) {
const label = getSelectedPointLabel(selectedEditPoint);
if (editDragMoved) {
setManualStatus(`Punto spostato (${label}). Puoi calcolare di nuovo oppure continuare a modificare.`);
} else {
setManualStatus(`Punto selezionato (${label}). Trascinalo per spostarlo, usa “Elimina punto”, oppure doppio click su un segmento per inserire un punto.`);
}
}
editDragMoved = false;
redrawManualOverlay();
updateManualPreview();
return;
}
};
manualOverlayCanvas.onclick = function(e) {
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
if (currentTool === 'edit') {
selectedEditPoint = findNearestEditablePoint(pos, POINT_HIT_RADIUS);
redrawManualOverlay();
if (selectedEditPoint) {
setManualStatus(`Punto selezionato (${getSelectedPointLabel(selectedEditPoint)}). Trascina per spostarlo o clicca “Elimina punto”.`);
} else {
setManualStatus('Nessun punto selezionato. Clicca vicino a un punto oppure doppio click su un segmento per aggiungerne uno.');
}
return;
}
if (currentTool === 'calibration') {
handleCalibrationClick(pos);
return;
@@ -1078,6 +1169,30 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
};
manualOverlayCanvas.ondblclick = function(e) {
if (currentTool !== 'edit') {
return;
}
e.preventDefault();
const pos = getCanvasMousePosition(e, manualOverlayCanvas);
const inserted = insertPointOnNearestSegment(pos, SEGMENT_HIT_RADIUS);
if (inserted) {
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
setManualStatus(`Nuovo punto inserito (${getSelectedPointLabel(selectedEditPoint)}). Ora puoi trascinarlo per rifinire il profilo.`);
} else {
Swal.fire({
icon: 'info',
title: 'Nessun segmento vicino',
text: 'Fai doppio click vicino a una linea del contorno esterno o di una esclusione.'
});
}
};
}
function handleCalibrationClick(pos) {
@@ -1153,6 +1268,12 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
polygonPoints.push(pos);
selectedEditPoint = {
type: 'outer',
pointIndex: polygonPoints.length - 1
};
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
}
@@ -1177,9 +1298,12 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
currentHolePoints.push(pos);
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
selectedEditPoint = {
type: 'currentHole',
pointIndex: currentHolePoints.length - 1
};
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
}
@@ -1196,9 +1320,9 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
holes.push([...currentHolePoints]);
currentHolePoints = [];
selectedEditPoint = null;
lastManualResult = null;
document.getElementById('saveManualAreaBtn').disabled = true;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
@@ -1276,22 +1400,27 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
manualOverlayCtx.stroke();
polygonPoints.forEach(point => {
drawPoint(point, 'rgba(22, 163, 74, 1)', 4);
polygonPoints.forEach((point, index) => {
const selected = isSelectedPoint('outer', null, index);
drawPoint(point, selected ? 'rgba(245, 158, 11, 1)' : 'rgba(22, 163, 74, 1)', selected ? 7 : 4);
if (selected) {
drawPointRing(point, 'rgba(245, 158, 11, 1)', 11);
}
});
}
function drawHoles() {
holes.forEach(hole => {
drawHolePolygon(hole, true);
holes.forEach((hole, holeIndex) => {
drawHolePolygon(hole, true, holeIndex, 'hole');
});
if (currentHolePoints.length > 0) {
drawHolePolygon(currentHolePoints, false);
drawHolePolygon(currentHolePoints, false, null, 'currentHole');
}
}
function drawHolePolygon(points, closed) {
function drawHolePolygon(points, closed, holeIndex = null, type = 'hole') {
if (!points || points.length === 0) {
return;
}
@@ -1314,8 +1443,13 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
manualOverlayCtx.stroke();
points.forEach(point => {
drawPoint(point, 'rgba(220, 38, 38, 1)', 4);
points.forEach((point, index) => {
const selected = isSelectedPoint(type, holeIndex, index);
drawPoint(point, selected ? 'rgba(245, 158, 11, 1)' : 'rgba(220, 38, 38, 1)', selected ? 7 : 4);
if (selected) {
drawPointRing(point, 'rgba(245, 158, 11, 1)', 11);
}
});
}
@@ -1326,6 +1460,296 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
manualOverlayCtx.fill();
}
function drawPointRing(point, color, radius) {
manualOverlayCtx.beginPath();
manualOverlayCtx.arc(point.x, point.y, radius, 0, Math.PI * 2);
manualOverlayCtx.strokeStyle = color;
manualOverlayCtx.lineWidth = 2;
manualOverlayCtx.stroke();
}
function markManualResultDirty() {
lastManualResult = null;
const saveBtn = document.getElementById('saveManualAreaBtn');
if (saveBtn) {
saveBtn.disabled = true;
}
}
function isSelectedPoint(type, holeIndex, pointIndex) {
if (!selectedEditPoint) {
return false;
}
return (
selectedEditPoint.type === type &&
selectedEditPoint.pointIndex === pointIndex &&
(
type !== 'hole' ||
selectedEditPoint.holeIndex === holeIndex
)
);
}
function getSelectedPointLabel(selection) {
if (!selection) {
return 'nessun punto';
}
if (selection.type === 'outer') {
return `contorno esterno, punto ${selection.pointIndex + 1}`;
}
if (selection.type === 'hole') {
return `esclusione ${selection.holeIndex + 1}, punto ${selection.pointIndex + 1}`;
}
if (selection.type === 'currentHole') {
return `esclusione attiva, punto ${selection.pointIndex + 1}`;
}
return 'punto';
}
function getEditablePointList() {
const refs = [];
polygonPoints.forEach((point, pointIndex) => {
refs.push({
type: 'outer',
holeIndex: null,
pointIndex: pointIndex,
point: point
});
});
holes.forEach((hole, holeIndex) => {
hole.forEach((point, pointIndex) => {
refs.push({
type: 'hole',
holeIndex: holeIndex,
pointIndex: pointIndex,
point: point
});
});
});
currentHolePoints.forEach((point, pointIndex) => {
refs.push({
type: 'currentHole',
holeIndex: null,
pointIndex: pointIndex,
point: point
});
});
return refs;
}
function findNearestEditablePoint(pos, radius) {
let best = null;
let bestDistance = radius;
getEditablePointList().forEach(ref => {
const d = distance(pos, ref.point);
if (d <= bestDistance) {
bestDistance = d;
best = {
type: ref.type,
holeIndex: ref.holeIndex,
pointIndex: ref.pointIndex
};
}
});
return best;
}
function getPointArrayBySelection(selection) {
if (!selection) {
return null;
}
if (selection.type === 'outer') {
return polygonPoints;
}
if (selection.type === 'hole') {
return holes[selection.holeIndex] || null;
}
if (selection.type === 'currentHole') {
return currentHolePoints;
}
return null;
}
function updateSelectedPointPosition(pos) {
const arr = getPointArrayBySelection(selectedEditPoint);
if (!arr || !selectedEditPoint) {
return;
}
arr[selectedEditPoint.pointIndex] = {
x: pos.x,
y: pos.y
};
}
function deleteSelectedPoint() {
if (!selectedEditPoint) {
Swal.fire({
icon: 'info',
title: 'Nessun punto selezionato',
text: 'Clicca prima un punto in modalità “Modifica punti”.'
});
return;
}
const arr = getPointArrayBySelection(selectedEditPoint);
if (!arr) {
selectedEditPoint = null;
redrawManualOverlay();
return;
}
if (selectedEditPoint.type === 'outer' && arr.length <= 3) {
Swal.fire({
icon: 'warning',
title: 'Non posso eliminare',
text: 'Il contorno esterno deve avere almeno 3 punti.'
});
return;
}
if (selectedEditPoint.type === 'hole' && arr.length <= 3) {
Swal.fire({
icon: 'warning',
title: 'Non posso eliminare',
text: 'Una esclusione chiusa deve avere almeno 3 punti. Usa “Annulla punto” in modalità esclusione per rimuovere lintera esclusione.'
});
return;
}
arr.splice(selectedEditPoint.pointIndex, 1);
selectedEditPoint = null;
markManualResultDirty();
redrawManualOverlay();
updateManualPreview();
setManualStatus('Punto eliminato. Ricontrolla il profilo e calcola di nuovo larea.');
}
function pointToSegmentDistance(p, a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
if (dx === 0 && dy === 0) {
return distance(p, a);
}
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy);
t = Math.max(0, Math.min(1, t));
const projection = {
x: a.x + t * dx,
y: a.y + t * dy
};
return distance(p, projection);
}
function findNearestSegment(pos, radius) {
const candidates = [];
function addSegments(points, type, holeIndex, closed) {
if (!points || points.length < 2) {
return;
}
const limit = closed ? points.length : points.length - 1;
for (let i = 0; i < limit; i++) {
const nextIndex = (i + 1) % points.length;
if (!closed && nextIndex === 0) {
continue;
}
candidates.push({
type: type,
holeIndex: holeIndex,
insertIndex: i + 1,
a: points[i],
b: points[nextIndex],
distance: pointToSegmentDistance(pos, points[i], points[nextIndex])
});
}
}
addSegments(polygonPoints, 'outer', null, polygonPoints.length >= 3);
holes.forEach((hole, holeIndex) => {
addSegments(hole, 'hole', holeIndex, hole.length >= 3);
});
addSegments(currentHolePoints, 'currentHole', null, false);
let best = null;
candidates.forEach(candidate => {
if (candidate.distance <= radius && (!best || candidate.distance < best.distance)) {
best = candidate;
}
});
return best;
}
function insertPointOnNearestSegment(pos, radius) {
const segment = findNearestSegment(pos, radius);
if (!segment) {
return false;
}
let arr = null;
if (segment.type === 'outer') {
arr = polygonPoints;
} else if (segment.type === 'hole') {
arr = holes[segment.holeIndex];
} else if (segment.type === 'currentHole') {
arr = currentHolePoints;
}
if (!arr) {
return false;
}
arr.splice(segment.insertIndex, 0, {
x: pos.x,
y: pos.y
});
selectedEditPoint = {
type: segment.type,
holeIndex: segment.holeIndex,
pointIndex: segment.insertIndex
};
return true;
}
function calculateManualArea() {
if (!mmPerPx || !calibrationMm || !calibrationPx) {
Swal.fire({
@@ -1589,6 +2013,10 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
parts.push(`Punti esclusione attiva: ${currentHolePoints.length}`);
}
if (selectedEditPoint) {
parts.push(`Selezionato: ${getSelectedPointLabel(selectedEditPoint)}`);
}
document.getElementById('manualCoordsPreview').innerText =
parts.length ? parts.join(' | ') : 'Nessun dato calcolato.';
}
@@ -1605,6 +2033,17 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
holeBtn.classList.remove('active');
}
const editBtn = document.getElementById('toolEditBtn');
if (editBtn) {
editBtn.classList.remove('active');
}
if (tool !== 'edit') {
selectedEditPoint = null;
isDraggingEditPoint = false;
editDragMoved = false;
}
if (tool === 'roi') {
document.getElementById('toolRoiBtn').classList.add('active');
@@ -1629,6 +2068,14 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
document.getElementById('toolHoleBtn').classList.add('active');
setManualStatus('Modalità area da escludere: clicca i punti del foro/cavità da sottrarre, poi clicca “Chiudi esclusione”.');
}
if (tool === 'edit') {
document.getElementById('toolEditBtn').classList.add('active');
setManualStatus('Modalità modifica punti: clicca e trascina un punto per spostarlo. Doppio click su un segmento per aggiungere un punto. Usa “Elimina punto” per cancellare quello selezionato.');
}
redrawManualOverlay();
updateManualPreview();
}
function setManualStatus(text) {
@@ -1720,6 +2167,25 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
});
document.getElementById('toolEditBtn').addEventListener('click', function() {
if (polygonPoints.length < 3 && holes.length === 0 && currentHolePoints.length === 0) {
Swal.fire({
icon: 'info',
title: 'Nessun profilo da modificare',
text: 'Disegna prima il contorno esterno oppure una esclusione.'
});
return;
}
setTool('edit');
selectedEditPoint = null;
redrawManualOverlay();
});
document.getElementById('deleteSelectedPointBtn').addEventListener('click', function() {
deleteSelectedPoint();
});
document.getElementById('toolHoleBtn').addEventListener('click', function() {
if (!mmPerPx) {
Swal.fire({