fixed cad area and added dfx

This commit is contained in:
2026-07-06 11:06:33 +02:00
parent 376252e263
commit db6e73bde5
6 changed files with 2679 additions and 94 deletions
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddDxfToCadAreaJobs extends AbstractMigration
{
public function change(): void
{
$this->table('cad_area_jobs')
->addColumn('dxf_filename', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'manual_holes_json'
])
->addColumn('dxf_url', 'string', [
'limit' => 500,
'null' => true,
'default' => null,
'after' => 'dxf_filename'
])
->update();
}
}
+216 -92
View File
@@ -160,7 +160,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
#cadAreaTable th:nth-child(2),
#cadAreaTable td:nth-child(2) {
width: 260px;
width: auto;
}
#cadAreaTable th:nth-child(3),
@@ -171,20 +171,13 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
#cadAreaTable th:nth-child(4),
#cadAreaTable td:nth-child(4),
#cadAreaTable th:nth-child(5),
#cadAreaTable td:nth-child(5),
#cadAreaTable th:nth-child(6),
#cadAreaTable td:nth-child(6) {
#cadAreaTable td:nth-child(5) {
width: 120px;
}
#cadAreaTable th:nth-child(7),
#cadAreaTable td:nth-child(7) {
width: 160px;
}
#cadAreaTable th:nth-child(8),
#cadAreaTable td:nth-child(8) {
width: 360px;
#cadAreaTable th:nth-child(6),
#cadAreaTable td:nth-child(6) {
width: 190px;
}
.processing-overlay {
@@ -353,10 +346,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
<th>Stato</th>
<th>Area mm²</th>
<th>Area cm²</th>
<th>Metodo</th>
<th>Confidenza</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
@@ -376,9 +366,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$areaMm2 = $job['area_mm2'] !== null ? number_format((float)$job['area_mm2'], 3, ',', '.') : '-';
$areaCm2 = $job['area_cm2'] !== null ? number_format((float)$job['area_cm2'], 4, ',', '.') : '-';
$method = $job['strategy_used'] ?: '-';
$confidence = $job['confidence'] ?: '-';
$fileUrl = htmlspecialchars($job['file_url'] ?? '', ENT_QUOTES, 'UTF-8');
$dxfUrl = htmlspecialchars($job['dxf_url'] ?? '', ENT_QUOTES, 'UTF-8');
?>
<tr data-id="<?= (int)$job['id']; ?>">
<td><?= (int)$job['id']; ?></td>
@@ -390,24 +379,29 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
<td><?= $badge; ?></td>
<td class="fw-semibold"><?= $areaMm2; ?></td>
<td class="fw-semibold"><?= $areaCm2; ?></td>
<td><?= htmlspecialchars((string)$method, ENT_QUOTES, 'UTF-8'); ?></td>
<td><?= htmlspecialchars((string)$confidence, ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<div class="action-buttons">
<?php if (!empty($job['file_url'])): ?>
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark">
📄 Apri
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark" title="Apri PDF">
📄
</a>
<button
class="btn btn-sm btn-outline-success manual-trace"
data-id="<?= (int)$job['id']; ?>"
data-url="<?= $fileUrl; ?>">
✏️ Traccia Manuale
data-url="<?= $fileUrl; ?>"
title="Traccia Manuale">
✏️
</button>
<?php endif; ?>
<?php if (!empty($job['dxf_url'])): ?>
<a href="<?= $dxfUrl; ?>" download class="btn btn-sm btn-outline-primary">
📐 DXF
</a>
<?php endif; ?>
<button class="btn btn-sm btn-outline-danger delete-one" data-id="<?= (int)$job['id']; ?>">
🗑️
</button>
@@ -540,6 +534,10 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
Chiudi
</button>
<button type="button" id="exportDxfBtn" class="btn btn-outline-primary" disabled>
📐 Esporta DXF
</button>
<button type="button" id="saveManualAreaBtn" class="btn btn-add" disabled>
💾 Salva area
</button>
@@ -607,9 +605,15 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
let edgeSnapEnabled = false;
let edgeSnapPreviewPoint = null;
const EDGE_SNAP_RADIUS = 45;
const EDGE_DARK_THRESHOLD = 245;
const EDGE_GRADIENT_THRESHOLD = 8;
const EDGE_SNAP_RADIUS = 50;
const EDGE_DARK_THRESHOLD = 200;
let edgeGray = null;
let edgeGrayW = 0;
let edgeGrayH = 0;
let snapRafPending = false;
let lastSnapMousePos = null;
let edgeCacheDirty = true;
let lastManualResult = null;
@@ -863,6 +867,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
manualCurrentView = 'full';
manualRoiPageRect = null;
edgeCacheDirty = true;
const viewport = manualPdfPage.getViewport({
scale: manualBaseScale
});
@@ -880,6 +886,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
canvasContext: manualPdfCtx,
viewport: viewport
}).promise.then(() => {
rebuildEdgeCache();
updateManualViewBadge();
redrawManualOverlay();
});
@@ -953,6 +960,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
manualCurrentView = 'roi';
edgeCacheDirty = true;
const scale = manualBaseScale * MANUAL_ROI_ZOOM_FACTOR;
const viewport = manualPdfPage.getViewport({
@@ -983,6 +991,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
viewport: viewport
}).promise.then(() => {
manualPdfCtx.restore();
rebuildEdgeCache();
updateManualViewBadge();
redrawManualOverlay();
}).catch(error => {
@@ -1067,6 +1076,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
document.getElementById('saveManualAreaBtn').disabled = true;
document.getElementById('manualCoordsPreview').innerText = 'Nessun dato calcolato.';
document.getElementById('exportDxfBtn').disabled = true;
edgeSnapEnabled = false;
@@ -1171,8 +1181,18 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
if (edgeSnapEnabled && ['polygon', 'hole', 'edit'].includes(currentTool)) {
edgeSnapPreviewPoint = snapToNearestEdge(pos, EDGE_SNAP_RADIUS);
lastSnapMousePos = pos;
if (!snapRafPending) {
snapRafPending = true;
requestAnimationFrame(() => {
snapRafPending = false;
edgeSnapPreviewPoint = snapToNearestEdge(lastSnapMousePos, EDGE_SNAP_RADIUS);
redrawManualOverlay();
});
}
return;
}
@@ -1318,96 +1338,119 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return pos;
}
function rebuildEdgeCache() {
edgeGray = null;
if (!manualPdfCanvas || !manualPdfCtx) {
return;
}
try {
const w = manualPdfCanvas.width;
const h = manualPdfCanvas.height;
const d = manualPdfCtx.getImageData(0, 0, w, h).data;
edgeGray = new Uint8Array(w * h);
for (let i = 0, p = 0; i < edgeGray.length; i++, p += 4) {
edgeGray[i] = (d[p] + d[p + 1] + d[p + 2]) / 3;
}
edgeGrayW = w;
edgeGrayH = h;
edgeCacheDirty = false;
console.log('Edge cache OK:', w + 'x' + h);
} catch (e) {
console.warn('Edge cache failed:', e);
edgeGray = null;
}
}
function snapToNearestEdge(pos, radius) {
const canvasWidth = manualPdfCanvas.width;
const canvasHeight = manualPdfCanvas.height;
if (edgeCacheDirty || !edgeGray || edgeGrayW !== manualPdfCanvas.width || edgeGrayH !== manualPdfCanvas.height) {
rebuildEdgeCache();
}
if (!edgeGray) {
return null;
}
const w = edgeGrayW;
const h = edgeGrayH;
const cx = Math.round(pos.x);
const cy = Math.round(pos.y);
const x0 = Math.max(1, cx - radius);
const y0 = Math.max(1, cy - radius);
const x1 = Math.min(canvasWidth - 2, cx + radius);
const y1 = Math.min(canvasHeight - 2, cy + radius);
const w = x1 - x0 + 1;
const h = y1 - y0 + 1;
if (w <= 3 || h <= 3) {
return null;
}
let imageData;
try {
imageData = manualPdfCtx.getImageData(x0, y0, w, h);
} catch (e) {
console.warn('Edge snap getImageData failed:', e);
return null;
}
const data = imageData.data;
function grayAt(localX, localY) {
const index = (localY * w + localX) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
return (r + g + b) / 3;
}
const x0 = Math.max(0, cx - radius);
const y0 = Math.max(0, cy - radius);
const x1 = Math.min(w - 1, cx + radius);
const y1 = Math.min(h - 1, cy + radius);
let best = null;
let bestScore = -Infinity;
let bestScore = Infinity;
const r2 = radius * radius;
for (let yy = 1; yy < h - 1; yy++) {
for (let xx = 1; xx < w - 1; xx++) {
const globalX = x0 + xx;
const globalY = y0 + yy;
for (let y = y0; y <= y1; y++) {
const rowOffset = y * w;
const dx = globalX - pos.x;
const dy = globalY - pos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
for (let x = x0; x <= x1; x++) {
const v = edgeGray[rowOffset + x];
if (dist > radius) {
if (v > EDGE_DARK_THRESHOLD) {
continue;
}
const center = grayAt(xx, yy);
const dx = x - pos.x;
const dy = y - pos.y;
const d2 = dx * dx + dy * dy;
const gx = Math.abs(grayAt(xx + 1, yy) - grayAt(xx - 1, yy));
const gy = Math.abs(grayAt(xx, yy + 1) - grayAt(xx, yy - 1));
const gradient = gx + gy;
const isDarkEnough = center < EDGE_DARK_THRESHOLD;
const isRealEdge = gradient > EDGE_GRADIENT_THRESHOLD;
if (!isDarkEnough && !isRealEdge) {
if (d2 > r2) {
continue;
}
/*
* Prefer:
* - dark strokes
* - real contrast edges
* - points close to the cursor
*/
const darknessScore = Math.max(0, 255 - center);
const gradientScore = gradient * 3;
const distancePenalty = dist * 4;
const score = Math.sqrt(d2) - (EDGE_DARK_THRESHOLD - v) * 0.03;
const score = darknessScore + gradientScore - distancePenalty;
if (score > bestScore) {
if (score < bestScore) {
bestScore = score;
best = {
x: globalX,
y: globalY
x: x,
y: y
};
}
}
}
if (!best) {
return null;
}
let sumX = 0;
let sumY = 0;
let sumW = 0;
for (let y = Math.max(0, best.y - 2); y <= Math.min(h - 1, best.y + 2); y++) {
for (let x = Math.max(0, best.x - 2); x <= Math.min(w - 1, best.x + 2); x++) {
const v = edgeGray[y * w + x];
if (v > EDGE_DARK_THRESHOLD) {
continue;
}
const weight = EDGE_DARK_THRESHOLD - v;
sumX += x * weight;
sumY += y * weight;
sumW += weight;
}
}
if (sumW > 0) {
return {
x: sumX / sumW,
y: sumY / sumW
};
}
return best;
}
@@ -1756,6 +1799,12 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (saveBtn) {
saveBtn.disabled = true;
}
const dxfBtn = document.getElementById('exportDxfBtn');
if (dxfBtn) {
dxfBtn.disabled = true;
}
}
function isSelectedPoint(type, holeIndex, pointIndex) {
@@ -2315,6 +2364,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
};
document.getElementById('saveManualAreaBtn').disabled = false;
document.getElementById('exportDxfBtn').disabled = false;
setManualStatus(
`Area finale: ${finalAreaMm2.toFixed(3)} mm² = ${finalAreaCm2.toFixed(4)} cm². ` +
@@ -2335,8 +2385,17 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return;
}
let dxfContent = '';
try {
dxfContent = buildDxfString();
} catch (e) {
console.warn('DXF build failed:', e);
}
const payload = {
id: currentManualJobId,
dxf_content: dxfContent,
area_mm2: +lastManualResult.area_mm2.toFixed(6),
area_cm2: +lastManualResult.area_cm2.toFixed(6),
@@ -2417,6 +2476,69 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
});
}
function toMmPoint(p) {
return {
x: p.x * mmPerPx,
y: (manualOverlayCanvas.height - p.y) * mmPerPx
};
}
function dxfPolyline(points, layer) {
let s = '0\nPOLYLINE\n8\n' + layer + '\n66\n1\n70\n1\n';
points.forEach(p => {
const m = toMmPoint(p);
s += '0\nVERTEX\n8\n' + layer +
'\n10\n' + m.x.toFixed(4) +
'\n20\n' + m.y.toFixed(4) +
'\n30\n0.0\n';
});
return s + '0\nSEQEND\n';
}
function buildDxfString() {
let dxf = '0\nSECTION\n2\nHEADER\n9\n$INSUNITS\n70\n4\n0\nENDSEC\n';
dxf += '0\nSECTION\n2\nENTITIES\n';
dxf += dxfPolyline(polygonPoints, 'PROFILO');
holes.forEach(hole => {
if (hole.length >= 3) {
dxf += dxfPolyline(hole, 'ESCLUSIONI');
}
});
dxf += '0\nENDSEC\n0\nEOF\n';
return dxf;
}
function exportDxf() {
if (!lastManualResult || !mmPerPx || polygonPoints.length < 3) {
Swal.fire({
icon: 'warning',
title: 'Prima calcola l\u2019area',
text: 'Il DXF viene generato dal profilo calibrato.'
});
return;
}
const dxf = buildDxfString();
const blob = new Blob([dxf], {
type: 'application/dxf'
});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'profilo_job_' + currentManualJobId + '.dxf';
a.click();
URL.revokeObjectURL(a.href);
}
document.getElementById('exportDxfBtn').addEventListener('click', function() {
exportDxf();
});
function polygonAreaPx2(points) {
let area = 0;
@@ -2634,6 +2756,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
btn.classList.add('btn-warning', 'active');
btn.innerText = '🧲 Magnete bordo ON';
rebuildEdgeCache();
setManualStatus(
'Magnete bordo attivo: quando disegni o sposti punti, il sistema prova ad agganciarli al bordo scuro più vicino.'
);
+52 -1
View File
@@ -50,6 +50,7 @@ try {
$outerPolygon = $input['manual_polygon'] ?? null;
$holes = $input['manual_holes'] ?? [];
$roi = $input['roi'] ?? null;
$dxfContent = isset($input['dxf_content']) ? (string)$input['dxf_content'] : '';
if ($areaMm2 <= 0) {
jsonResponse([
@@ -106,6 +107,43 @@ try {
$manualHolesJson = json_encode($holes);
$dxfFilename = null;
$dxfUrl = null;
if ($dxfContent !== '') {
if (strlen($dxfContent) > 5 * 1024 * 1024) {
jsonResponse([
'success' => false,
'message' => 'DXF troppo grande.'
]);
}
if (strpos($dxfContent, 'SECTION') === false || strpos($dxfContent, 'EOF') === false) {
jsonResponse([
'success' => false,
'message' => 'Contenuto DXF non valido.'
]);
}
$dxfDir = __DIR__ . '/uploads/cad_area/dxf/';
if (!is_dir($dxfDir)) {
mkdir($dxfDir, 0755, true);
}
$dxfFilename = 'profilo_' . $id . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.dxf';
if (file_put_contents($dxfDir . $dxfFilename, $dxfContent) === false) {
jsonResponse([
'success' => false,
'message' => 'Impossibile salvare il file DXF sul server.'
]);
}
$dxfUrl = 'uploads/cad_area/dxf/' . $dxfFilename;
}
$roiX = null;
$roiY = null;
$roiW = null;
@@ -152,6 +190,9 @@ try {
manual_holes_json = ?,
manual_status = 'completed',
dxf_filename = COALESCE(?, dxf_filename),
dxf_url = COALESCE(?, dxf_url),
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
@@ -188,6 +229,9 @@ try {
$manualPolygonJson,
$manualHolesJson,
$dxfFilename,
$dxfUrl,
$mmPerPx,
'manual',
@@ -227,6 +271,9 @@ try {
manual_holes_json = ?,
manual_status = 'completed',
dxf_filename = COALESCE(?, dxf_filename),
dxf_url = COALESCE(?, dxf_url),
scale_used = ?,
scale_detected = ?,
confidence = 'manual_validated',
@@ -264,6 +311,9 @@ try {
$manualPolygonJson,
$manualHolesJson,
$dxfFilename,
$dxfUrl,
$mmPerPx,
'manual',
@@ -285,7 +335,8 @@ try {
'area_mm2' => $areaMm2,
'area_cm2' => $areaCm2,
'outer_area_mm2' => $outerAreaMm2,
'holes_area_mm2' => $holesAreaMm2
'holes_area_mm2' => $holesAreaMm2,
'dxf_url' => $dxfUrl
]);
} catch (Throwable $e) {
error_log('CAD manual area save error: ' . $e->getMessage());
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,158 @@
0
SECTION
2
HEADER
9
$INSUNITS
70
4
0
ENDSEC
0
SECTION
2
ENTITIES
0
POLYLINE
8
PROFILO
66
1
70
1
0
VERTEX
8
PROFILO
10
5.9087
20
9.0157
30
0.0
0
VERTEX
8
PROFILO
10
6.0608
20
2.8408
30
0.0
0
VERTEX
8
PROFILO
10
11.0190
20
2.8256
30
0.0
0
VERTEX
8
PROFILO
10
16.7528
20
3.2362
30
0.0
0
VERTEX
8
PROFILO
10
17.9239
20
4.4834
30
0.0
0
VERTEX
8
PROFILO
10
17.7262
20
6.6887
30
0.0
0
VERTEX
8
PROFILO
10
17.1939
20
8.1336
30
0.0
0
VERTEX
8
PROFILO
10
16.0076
20
10.2476
30
0.0
0
VERTEX
8
PROFILO
10
15.3688
20
10.9320
30
0.0
0
VERTEX
8
PROFILO
10
13.4373
20
11.0233
30
0.0
0
VERTEX
8
PROFILO
10
9.0722
20
11.4339
30
0.0
0
VERTEX
8
PROFILO
10
8.1597
20
11.2210
30
0.0
0
VERTEX
8
PROFILO
10
7.3992
20
10.7343
30
0.0
0
SEQEND
0
ENDSEC
0
EOF
File diff suppressed because it is too large Load Diff