diff --git a/public/userarea/cad-area.php b/public/userarea/cad-area.php
index f118770..5d8cd55 100644
--- a/public/userarea/cad-area.php
+++ b/public/userarea/cad-area.php
@@ -234,12 +234,30 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
.manual-toolbar {
display: flex;
- gap: 8px;
+ gap: 6px 14px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
+ .toolbar-group {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ padding-right: 14px;
+ border-right: 1px solid #cbd5e1;
+ }
+
+ .toolbar-group:last-child {
+ border-right: none;
+ padding-right: 0;
+ }
+
+ .manual-view-badge {
+ padding: 5px 10px;
+ font-size: 0.82rem;
+ }
+
.manual-toolbar .btn.active {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.2);
font-weight: 700;
@@ -450,69 +468,73 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -615,6 +637,14 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
let lastSnapMousePos = null;
let edgeCacheDirty = true;
+ let isBrushing = false;
+ let lastBrushPoint = null;
+ let brushStartIndex = 0;
+
+ const BRUSH_STEP = 10;
+ const BRUSH_SIMPLIFY_EPSILON = 2.0;
+ const BRUSH_CONTINUITY_RATIO = 0.7;
+
let lastManualResult = null;
$(document).ready(function() {
@@ -1084,7 +1114,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (edgeSnapBtn) {
edgeSnapBtn.classList.remove('btn-warning', 'active');
edgeSnapBtn.classList.add('btn-outline-warning');
- edgeSnapBtn.innerText = '🧲 Magnete bordo OFF';
+ edgeSnapBtn.innerText = '🧲 OFF';
}
setTool('roi');
@@ -1108,7 +1138,27 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
+ if (currentTool === 'brush') {
+ if (!mmPerPx) {
+ Swal.fire({
+ icon: 'warning',
+ title: 'Calibrazione mancante',
+ text: 'Prima devi calibrare una quota nota.'
+ });
+ return;
+ }
+ isBrushing = true;
+ brushStartIndex = polygonPoints.length;
+
+ const startPos = snapToNearestEdge(pos, EDGE_SNAP_RADIUS) || pos;
+ polygonPoints.push(startPos);
+ lastBrushPoint = startPos;
+
+ markManualResultDirty();
+ redrawManualOverlay();
+ return;
+ }
if (currentTool === 'auto_roi') {
isDrawingAutoContourRoi = true;
autoContourRoiStartX = pos.x;
@@ -1170,7 +1220,34 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
redrawManualOverlay();
return;
}
+ if (currentTool === 'brush' && isBrushing) {
+ if (!lastBrushPoint || distance(pos, lastBrushPoint) < BRUSH_STEP) {
+ return;
+ }
+ let added = false;
+ let guard = 0;
+
+ while (guard < 25 && lastBrushPoint && distance(pos, lastBrushPoint) > BRUSH_STEP) {
+ guard++;
+
+ const next = brushWalkAlongEdge(lastBrushPoint, pos);
+
+ if (!next) {
+ break;
+ }
+
+ polygonPoints.push(next);
+ lastBrushPoint = next;
+ added = true;
+ }
+
+ if (added) {
+ redrawManualOverlay();
+ }
+
+ return;
+ }
if (currentTool === 'edit' && isDraggingEditPoint && selectedEditPoint) {
const finalPos = getSnappedPoint(pos);
updateSelectedPointPosition(finalPos);
@@ -1219,7 +1296,26 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
+ if (currentTool === 'brush' && isBrushing) {
+ isBrushing = false;
+ lastBrushPoint = null;
+ if (polygonPoints.length - brushStartIndex >= 3) {
+ const stroke = polygonPoints.slice(brushStartIndex);
+ const simplified = simplifyRdp(stroke, BRUSH_SIMPLIFY_EPSILON);
+ polygonPoints = polygonPoints.slice(0, brushStartIndex).concat(simplified);
+ }
+
+ markManualResultDirty();
+ redrawManualOverlay();
+ updateManualPreview();
+
+ setManualStatus(
+ 'Tratto completato: ' + polygonPoints.length + ' punti totali. ' +
+ 'Continua con un altro tratto, oppure passa a "Modifica punti" per rifinire e poi calcola l\u2019area.'
+ );
+ return;
+ }
if (currentTool === 'auto_roi' && isDrawingAutoContourRoi) {
isDrawingAutoContourRoi = false;
@@ -2638,6 +2734,193 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
parts.length ? parts.join(' | ') : 'Nessun dato calcolato.';
}
+ function segmentDarkRatio(a, b) {
+ const steps = Math.max(4, Math.round(Math.hypot(b.x - a.x, b.y - a.y)));
+ let dark = 0;
+
+ for (let i = 1; i <= steps; i++) {
+ const t = i / steps;
+ const x = Math.round(a.x + (b.x - a.x) * t);
+ const y = Math.round(a.y + (b.y - a.y) * t);
+
+ let found = false;
+
+ for (let yy = -1; yy <= 1 && !found; yy++) {
+ for (let xx = -1; xx <= 1 && !found; xx++) {
+ const px = x + xx;
+ const py = y + yy;
+
+ if (px < 0 || py < 0 || px >= edgeGrayW || py >= edgeGrayH) {
+ continue;
+ }
+
+ if (edgeGray[py * edgeGrayW + px] <= EDGE_DARK_THRESHOLD) {
+ found = true;
+ }
+ }
+ }
+
+ if (found) {
+ dark++;
+ }
+ }
+
+ return dark / steps;
+ }
+
+ function brushWalkAlongEdge(fromPoint, mousePos) {
+ if (edgeCacheDirty || !edgeGray) {
+ rebuildEdgeCache();
+ }
+
+ if (!edgeGray) {
+ return null;
+ }
+
+ const w = edgeGrayW;
+ const h = edgeGrayH;
+
+ const dirX = mousePos.x - fromPoint.x;
+ const dirY = mousePos.y - fromPoint.y;
+ const dirLen = Math.hypot(dirX, dirY);
+
+ if (dirLen < 0.001) {
+ return null;
+ }
+
+ const ndx = dirX / dirLen;
+ const ndy = dirY / dirLen;
+
+ const fx = Math.round(fromPoint.x);
+ const fy = Math.round(fromPoint.y);
+
+ let sx = -1;
+ let sy = -1;
+ let sBest = Infinity;
+
+ for (let y = Math.max(0, fy - 6); y <= Math.min(h - 1, fy + 6); y++) {
+ for (let x = Math.max(0, fx - 6); x <= Math.min(w - 1, fx + 6); x++) {
+ if (edgeGray[y * w + x] > EDGE_DARK_THRESHOLD) {
+ continue;
+ }
+
+ const d = (x - fromPoint.x) * (x - fromPoint.x) + (y - fromPoint.y) * (y - fromPoint.y);
+
+ if (d < sBest) {
+ sBest = d;
+ sx = x;
+ sy = y;
+ }
+ }
+ }
+
+ if (sx < 0) {
+ return null;
+ }
+
+ const R = Math.ceil(BRUSH_STEP * 1.8);
+ const x0 = Math.max(0, sx - R);
+ const y0 = Math.max(0, sy - R);
+ const x1 = Math.min(w - 1, sx + R);
+ const y1 = Math.min(h - 1, sy + R);
+ const lw = x1 - x0 + 1;
+
+ const visited = new Uint8Array(lw * (y1 - y0 + 1));
+ let queue = [sx, sy];
+ visited[(sy - y0) * lw + (sx - x0)] = 1;
+
+ let best = null;
+ let bestScore = -Infinity;
+ const minDist = BRUSH_STEP * 0.75;
+
+ while (queue.length) {
+ const next = [];
+
+ for (let q = 0; q < queue.length; q += 2) {
+ const cx = queue[q];
+ const cy = queue[q + 1];
+
+ const ddx = cx - fromPoint.x;
+ const ddy = cy - fromPoint.y;
+ const dist = Math.hypot(ddx, ddy);
+
+ if (dist >= minDist) {
+ const forward = (ddx * ndx + ddy * ndy) / dist;
+
+ if (forward > 0.05) {
+ const score = forward * 10 - Math.abs(dist - BRUSH_STEP) * 0.5;
+
+ if (score > bestScore) {
+ bestScore = score;
+ best = {
+ x: cx,
+ y: cy
+ };
+ }
+ }
+ }
+
+ for (let oy = -1; oy <= 1; oy++) {
+ for (let ox = -1; ox <= 1; ox++) {
+ if (ox === 0 && oy === 0) {
+ continue;
+ }
+
+ const nx = cx + ox;
+ const ny = cy + oy;
+
+ if (nx < x0 || ny < y0 || nx > x1 || ny > y1) {
+ continue;
+ }
+
+ const vi = (ny - y0) * lw + (nx - x0);
+
+ if (visited[vi]) {
+ continue;
+ }
+
+ if (edgeGray[ny * w + nx] > EDGE_DARK_THRESHOLD) {
+ continue;
+ }
+
+ visited[vi] = 1;
+ next.push(nx, ny);
+ }
+ }
+ }
+
+ queue = next;
+ }
+
+ return best;
+ }
+
+ function simplifyRdp(points, epsilon) {
+ if (points.length < 3) {
+ return points;
+ }
+
+ let maxDist = 0;
+ let maxIndex = 0;
+
+ for (let i = 1; i < points.length - 1; i++) {
+ const d = pointToSegmentDistance(points[i], points[0], points[points.length - 1]);
+
+ if (d > maxDist) {
+ maxDist = d;
+ maxIndex = i;
+ }
+ }
+
+ if (maxDist > epsilon) {
+ const left = simplifyRdp(points.slice(0, maxIndex + 1), epsilon);
+ const right = simplifyRdp(points.slice(maxIndex), epsilon);
+ return left.slice(0, -1).concat(right);
+ }
+
+ return [points[0], points[points.length - 1]];
+ }
+
function setTool(tool) {
currentTool = tool;
@@ -2649,7 +2932,13 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (autoRoiBtn) {
autoRoiBtn.classList.remove('active');
}
+ const brushBtn = document.getElementById('toolBrushBtn');
+ if (brushBtn) {
+ brushBtn.classList.remove('active');
+ }
+ isBrushing = false;
+ lastBrushPoint = null;
const holeBtn = document.getElementById('toolHoleBtn');
if (holeBtn) {
holeBtn.classList.remove('active');
@@ -2685,7 +2974,10 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
document.getElementById('toolPolygonBtn').classList.add('active');
setManualStatus('Modalità profilo esterno: clicca i punti del contorno esterno. Il poligono viene chiuso automaticamente.');
}
-
+ if (tool === 'brush') {
+ document.getElementById('toolBrushBtn').classList.add('active');
+ setManualStatus('Pennello magnetico: tieni premuto e trascina lungo il bordo. I punti si agganciano al tratto scuro. Rilascia per chiudere il tratto, poi rifinisci con "Modifica punti".');
+ }
if (tool === 'auto_roi') {
document.getElementById('toolAutoContourRoiBtn').classList.add('active');
setManualStatus('Modalità ROI autocontorno: disegna un rettangolo stretto solo attorno al profilo, escludendo quote, testi e frecce.');
@@ -2745,7 +3037,19 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
setTool('polygon');
});
+ document.getElementById('toolBrushBtn').addEventListener('click', function() {
+ if (!mmPerPx) {
+ Swal.fire({
+ icon: 'info',
+ title: 'Prima calibra la scala',
+ text: 'Clicca \u201cCalibra quota\u201d e seleziona due punti su una quota nota.'
+ });
+ return;
+ }
+ rebuildEdgeCache();
+ setTool('brush');
+ });
document.getElementById('edgeSnapBtn').addEventListener('click', function() {
edgeSnapEnabled = !edgeSnapEnabled;
@@ -2754,7 +3058,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (edgeSnapEnabled) {
btn.classList.remove('btn-outline-warning');
btn.classList.add('btn-warning', 'active');
- btn.innerText = '🧲 Magnete bordo ON';
+ btn.innerText = '🧲 ON';
rebuildEdgeCache();
@@ -2764,7 +3068,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
btn.classList.remove('btn-warning', 'active');
btn.classList.add('btn-outline-warning');
- btn.innerText = '🧲 Magnete bordo OFF';
+ btn.innerText = '🧲 OFF';
edgeSnapPreviewPoint = null;
redrawManualOverlay();
@@ -2893,22 +3197,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
renderManualRoiViewFromCurrentRoi();
});
- document.getElementById('toolAutoContourRoiBtn').addEventListener('click', function() {
- if (manualCurrentView !== 'roi') {
- Swal.fire({
- icon: 'info',
- title: 'Prima fai Zoom su ROI',
- text: 'La ROI autocontorno va disegnata dentro la vista ingrandita della sezione.'
- });
- return;
- }
- setTool('auto_roi');
- });
-
- document.getElementById('autoContourBtn').addEventListener('click', function() {
- proposeAutoContour();
- });
document.getElementById('fullPageBtn').addEventListener('click', function() {
Swal.fire({