From 4c09a0dcb40c07d26ee0b59473f002096c3079e1 Mon Sep 17 00:00:00 2001 From: solocla Date: Tue, 16 Jun 2026 09:44:19 +0200 Subject: [PATCH] cad area punti mpodifica manuale --- public/userarea/cad-area.php | 514 +++++++++++++++++++++++++++++++++-- 1 file changed, 490 insertions(+), 24 deletions(-) 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({