fixed button cad area
This commit is contained in:
+345
-56
@@ -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,70 +468,74 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
</div>
|
||||
|
||||
<div class="manual-toolbar mb-3">
|
||||
<button type="button" id="toolRoiBtn" class="btn btn-outline-primary active">
|
||||
🎯 1. ROI
|
||||
<div class="toolbar-group">
|
||||
<button type="button" id="toolRoiBtn" class="btn btn-sm btn-outline-primary active" title="Disegna un rettangolo attorno alla sezione">
|
||||
🎯 ROI
|
||||
</button>
|
||||
|
||||
<button type="button" id="toolCalibrationBtn" class="btn btn-outline-danger">
|
||||
📏 2. Calibra quota
|
||||
<button type="button" id="zoomRoiBtn" class="btn btn-sm btn-outline-info" title="Ingrandisci la zona selezionata">
|
||||
🔍 Zoom
|
||||
</button>
|
||||
|
||||
<button type="button" id="toolPolygonBtn" class="btn btn-outline-success">
|
||||
✏️ 3. Disegna profilo
|
||||
<button type="button" id="fullPageBtn" class="btn btn-sm btn-outline-dark" disabled title="Torna alla pagina intera">
|
||||
↩️ Pagina
|
||||
</button>
|
||||
|
||||
<button type="button" id="edgeSnapBtn" class="btn btn-outline-warning">
|
||||
🧲 Magnete bordo OFF
|
||||
<button type="button" id="toolCalibrationBtn" class="btn btn-sm btn-outline-danger" title="Clicca due punti su una quota nota">
|
||||
📏 Calibra
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button type="button" id="toolPolygonBtn" class="btn btn-sm btn-outline-success" title="Clicca i punti del contorno esterno">
|
||||
✏️ Profilo
|
||||
</button>
|
||||
|
||||
<button type="button" id="toolHoleBtn" class="btn btn-outline-danger">
|
||||
➖ 4. Area da escludere
|
||||
<button type="button" id="toolBrushBtn" class="btn btn-sm btn-outline-success" title="Trascina lungo il bordo per tracciare">
|
||||
🖌️ Pennello
|
||||
</button>
|
||||
|
||||
<button type="button" id="closeHoleBtn" class="btn btn-outline-danger">
|
||||
✅ Chiudi esclusione
|
||||
<button type="button" id="edgeSnapBtn" class="btn btn-sm btn-outline-warning" title="Aggancia i punti al bordo scuro più vicino">
|
||||
🧲 OFF
|
||||
</button>
|
||||
|
||||
<button type="button" id="toolEditBtn" class="btn btn-outline-secondary">
|
||||
✋ Modifica punti
|
||||
<button type="button" id="toolHoleBtn" class="btn btn-sm btn-outline-danger" title="Traccia un foro/cavità da sottrarre">
|
||||
➖ Esclusione
|
||||
</button>
|
||||
|
||||
<button type="button" id="undoPointBtn" class="btn btn-outline-secondary">
|
||||
↩️ Annulla punto
|
||||
<button type="button" id="closeHoleBtn" class="btn btn-sm btn-outline-danger" title="Chiudi l'esclusione corrente">
|
||||
✅ Chiudi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button type="button" id="toolEditBtn" class="btn btn-sm btn-outline-secondary" title="Sposta, elimina o inserisci punti">
|
||||
✋ Modifica
|
||||
</button>
|
||||
|
||||
<button type="button" id="deleteSelectedPointBtn" class="btn btn-outline-danger">
|
||||
❌ Elimina punto
|
||||
<button type="button" id="undoPointBtn" class="btn btn-sm btn-outline-secondary" title="Rimuovi l'ultimo punto inserito">
|
||||
↩️ Annulla
|
||||
</button>
|
||||
|
||||
<button type="button" id="resetManualBtn" class="btn btn-outline-warning">
|
||||
<button type="button" id="deleteSelectedPointBtn" class="btn btn-sm btn-outline-danger" title="Elimina il punto selezionato">
|
||||
❌ Elimina
|
||||
</button>
|
||||
|
||||
<button type="button" id="resetManualBtn" class="btn btn-sm btn-outline-warning" title="Cancella tutto e ricomincia">
|
||||
🔄 Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" id="zoomRoiBtn" class="btn btn-outline-info">
|
||||
🔍 Zoom su ROI
|
||||
</button>
|
||||
|
||||
<button type="button" id="toolAutoContourRoiBtn" class="btn btn-outline-info">
|
||||
🎯 ROI autocontorno
|
||||
</button>
|
||||
|
||||
<button type="button" id="autoContourBtn" class="btn btn-outline-primary">
|
||||
🤖 Proponi contorno
|
||||
</button>
|
||||
|
||||
<button type="button" id="fullPageBtn" class="btn btn-outline-dark" disabled>
|
||||
↩️ Pagina intera
|
||||
</button>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<span id="manualViewBadge" class="manual-view-badge">
|
||||
Vista: pagina intera
|
||||
</span>
|
||||
|
||||
<button type="button" id="calculateManualBtn" class="btn btn-add">
|
||||
<button type="button" id="calculateManualBtn" class="btn btn-sm btn-add">
|
||||
🧮 Calcola area
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-result-box mb-3" id="manualStatusBox">
|
||||
Modalità attiva: ROI. Disegna un rettangolo attorno alla sezione.
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user