diff --git a/public/userarea/cad-area.php b/public/userarea/cad-area.php
index 8120c2c..59a36c9 100644
--- a/public/userarea/cad-area.php
+++ b/public/userarea/cad-area.php
@@ -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;
+ }
@@ -467,10 +476,18 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
✅ Chiudi esclusione
+
+
+
+
@@ -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 l’intera esclusione.'
+ });
+ return;
+ }
+
+ arr.splice(selectedEditPoint.pointIndex, 1);
+
+ selectedEditPoint = null;
+ markManualResultDirty();
+ redrawManualOverlay();
+ updateManualPreview();
+
+ setManualStatus('Punto eliminato. Ricontrolla il profilo e calcola di nuovo l’area.');
+ }
+
+ 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({