fixed cad area and added dfx
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+217
-93
@@ -160,7 +160,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
#cadAreaTable th:nth-child(2),
|
#cadAreaTable th:nth-child(2),
|
||||||
#cadAreaTable td:nth-child(2) {
|
#cadAreaTable td:nth-child(2) {
|
||||||
width: 260px;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cadAreaTable th:nth-child(3),
|
#cadAreaTable th:nth-child(3),
|
||||||
@@ -171,20 +171,13 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
#cadAreaTable th:nth-child(4),
|
#cadAreaTable th:nth-child(4),
|
||||||
#cadAreaTable td:nth-child(4),
|
#cadAreaTable td:nth-child(4),
|
||||||
#cadAreaTable th:nth-child(5),
|
#cadAreaTable th:nth-child(5),
|
||||||
#cadAreaTable td:nth-child(5),
|
#cadAreaTable td:nth-child(5) {
|
||||||
#cadAreaTable th:nth-child(6),
|
|
||||||
#cadAreaTable td:nth-child(6) {
|
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cadAreaTable th:nth-child(7),
|
#cadAreaTable th:nth-child(6),
|
||||||
#cadAreaTable td:nth-child(7) {
|
#cadAreaTable td:nth-child(6) {
|
||||||
width: 160px;
|
width: 190px;
|
||||||
}
|
|
||||||
|
|
||||||
#cadAreaTable th:nth-child(8),
|
|
||||||
#cadAreaTable td:nth-child(8) {
|
|
||||||
width: 360px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.processing-overlay {
|
.processing-overlay {
|
||||||
@@ -353,10 +346,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th>Area mm²</th>
|
<th>Area mm²</th>
|
||||||
<th>Area cm²</th>
|
<th>Area cm²</th>
|
||||||
<th>Metodo</th>
|
|
||||||
<th>Confidenza</th>
|
|
||||||
<th>Azioni</th>
|
<th>Azioni</th>
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -376,9 +366,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
$areaMm2 = $job['area_mm2'] !== null ? number_format((float)$job['area_mm2'], 3, ',', '.') : '-';
|
$areaMm2 = $job['area_mm2'] !== null ? number_format((float)$job['area_mm2'], 3, ',', '.') : '-';
|
||||||
$areaCm2 = $job['area_cm2'] !== null ? number_format((float)$job['area_cm2'], 4, ',', '.') : '-';
|
$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');
|
$fileUrl = htmlspecialchars($job['file_url'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||||
|
$dxfUrl = htmlspecialchars($job['dxf_url'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||||
?>
|
?>
|
||||||
<tr data-id="<?= (int)$job['id']; ?>">
|
<tr data-id="<?= (int)$job['id']; ?>">
|
||||||
<td><?= (int)$job['id']; ?></td>
|
<td><?= (int)$job['id']; ?></td>
|
||||||
@@ -390,24 +379,29 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<td><?= $badge; ?></td>
|
<td><?= $badge; ?></td>
|
||||||
<td class="fw-semibold"><?= $areaMm2; ?></td>
|
<td class="fw-semibold"><?= $areaMm2; ?></td>
|
||||||
<td class="fw-semibold"><?= $areaCm2; ?></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>
|
<td>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<?php if (!empty($job['file_url'])): ?>
|
<?php if (!empty($job['file_url'])): ?>
|
||||||
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark">
|
<a href="<?= $fileUrl; ?>" target="_blank" class="btn btn-sm btn-outline-dark" title="Apri PDF">
|
||||||
📄 Apri
|
📄
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-success manual-trace"
|
class="btn btn-sm btn-outline-success manual-trace"
|
||||||
data-id="<?= (int)$job['id']; ?>"
|
data-id="<?= (int)$job['id']; ?>"
|
||||||
data-url="<?= $fileUrl; ?>">
|
data-url="<?= $fileUrl; ?>"
|
||||||
✏️ Traccia Manuale
|
title="Traccia Manuale">
|
||||||
|
✏️
|
||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?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 class="btn btn-sm btn-outline-danger delete-one" data-id="<?= (int)$job['id']; ?>">
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
@@ -540,6 +534,10 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
Chiudi
|
Chiudi
|
||||||
</button>
|
</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>
|
<button type="button" id="saveManualAreaBtn" class="btn btn-add" disabled>
|
||||||
💾 Salva area
|
💾 Salva area
|
||||||
</button>
|
</button>
|
||||||
@@ -607,9 +605,15 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
let edgeSnapEnabled = false;
|
let edgeSnapEnabled = false;
|
||||||
let edgeSnapPreviewPoint = null;
|
let edgeSnapPreviewPoint = null;
|
||||||
|
|
||||||
const EDGE_SNAP_RADIUS = 45;
|
const EDGE_SNAP_RADIUS = 50;
|
||||||
const EDGE_DARK_THRESHOLD = 245;
|
const EDGE_DARK_THRESHOLD = 200;
|
||||||
const EDGE_GRADIENT_THRESHOLD = 8;
|
|
||||||
|
let edgeGray = null;
|
||||||
|
let edgeGrayW = 0;
|
||||||
|
let edgeGrayH = 0;
|
||||||
|
let snapRafPending = false;
|
||||||
|
let lastSnapMousePos = null;
|
||||||
|
let edgeCacheDirty = true;
|
||||||
|
|
||||||
let lastManualResult = null;
|
let lastManualResult = null;
|
||||||
|
|
||||||
@@ -863,6 +867,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
manualCurrentView = 'full';
|
manualCurrentView = 'full';
|
||||||
manualRoiPageRect = null;
|
manualRoiPageRect = null;
|
||||||
|
|
||||||
|
edgeCacheDirty = true;
|
||||||
|
|
||||||
const viewport = manualPdfPage.getViewport({
|
const viewport = manualPdfPage.getViewport({
|
||||||
scale: manualBaseScale
|
scale: manualBaseScale
|
||||||
});
|
});
|
||||||
@@ -880,6 +886,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
canvasContext: manualPdfCtx,
|
canvasContext: manualPdfCtx,
|
||||||
viewport: viewport
|
viewport: viewport
|
||||||
}).promise.then(() => {
|
}).promise.then(() => {
|
||||||
|
rebuildEdgeCache();
|
||||||
updateManualViewBadge();
|
updateManualViewBadge();
|
||||||
redrawManualOverlay();
|
redrawManualOverlay();
|
||||||
});
|
});
|
||||||
@@ -953,6 +960,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
}
|
}
|
||||||
|
|
||||||
manualCurrentView = 'roi';
|
manualCurrentView = 'roi';
|
||||||
|
edgeCacheDirty = true;
|
||||||
|
|
||||||
const scale = manualBaseScale * MANUAL_ROI_ZOOM_FACTOR;
|
const scale = manualBaseScale * MANUAL_ROI_ZOOM_FACTOR;
|
||||||
const viewport = manualPdfPage.getViewport({
|
const viewport = manualPdfPage.getViewport({
|
||||||
@@ -983,6 +991,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
viewport: viewport
|
viewport: viewport
|
||||||
}).promise.then(() => {
|
}).promise.then(() => {
|
||||||
manualPdfCtx.restore();
|
manualPdfCtx.restore();
|
||||||
|
rebuildEdgeCache();
|
||||||
updateManualViewBadge();
|
updateManualViewBadge();
|
||||||
redrawManualOverlay();
|
redrawManualOverlay();
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
@@ -1067,6 +1076,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
document.getElementById('saveManualAreaBtn').disabled = true;
|
document.getElementById('saveManualAreaBtn').disabled = true;
|
||||||
document.getElementById('manualCoordsPreview').innerText = 'Nessun dato calcolato.';
|
document.getElementById('manualCoordsPreview').innerText = 'Nessun dato calcolato.';
|
||||||
|
document.getElementById('exportDxfBtn').disabled = true;
|
||||||
|
|
||||||
edgeSnapEnabled = false;
|
edgeSnapEnabled = false;
|
||||||
|
|
||||||
@@ -1171,8 +1181,18 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (edgeSnapEnabled && ['polygon', 'hole', 'edit'].includes(currentTool)) {
|
if (edgeSnapEnabled && ['polygon', 'hole', 'edit'].includes(currentTool)) {
|
||||||
edgeSnapPreviewPoint = snapToNearestEdge(pos, EDGE_SNAP_RADIUS);
|
lastSnapMousePos = pos;
|
||||||
redrawManualOverlay();
|
|
||||||
|
if (!snapRafPending) {
|
||||||
|
snapRafPending = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
snapRafPending = false;
|
||||||
|
edgeSnapPreviewPoint = snapToNearestEdge(lastSnapMousePos, EDGE_SNAP_RADIUS);
|
||||||
|
redrawManualOverlay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,96 +1338,119 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
return pos;
|
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) {
|
function snapToNearestEdge(pos, radius) {
|
||||||
const canvasWidth = manualPdfCanvas.width;
|
if (edgeCacheDirty || !edgeGray || edgeGrayW !== manualPdfCanvas.width || edgeGrayH !== manualPdfCanvas.height) {
|
||||||
const canvasHeight = manualPdfCanvas.height;
|
rebuildEdgeCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!edgeGray) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = edgeGrayW;
|
||||||
|
const h = edgeGrayH;
|
||||||
|
|
||||||
const cx = Math.round(pos.x);
|
const cx = Math.round(pos.x);
|
||||||
const cy = Math.round(pos.y);
|
const cy = Math.round(pos.y);
|
||||||
|
|
||||||
const x0 = Math.max(1, cx - radius);
|
const x0 = Math.max(0, cx - radius);
|
||||||
const y0 = Math.max(1, cy - radius);
|
const y0 = Math.max(0, cy - radius);
|
||||||
const x1 = Math.min(canvasWidth - 2, cx + radius);
|
const x1 = Math.min(w - 1, cx + radius);
|
||||||
const y1 = Math.min(canvasHeight - 2, cy + radius);
|
const y1 = Math.min(h - 1, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
let best = null;
|
let best = null;
|
||||||
let bestScore = -Infinity;
|
let bestScore = Infinity;
|
||||||
|
const r2 = radius * radius;
|
||||||
|
|
||||||
for (let yy = 1; yy < h - 1; yy++) {
|
for (let y = y0; y <= y1; y++) {
|
||||||
for (let xx = 1; xx < w - 1; xx++) {
|
const rowOffset = y * w;
|
||||||
const globalX = x0 + xx;
|
|
||||||
const globalY = y0 + yy;
|
|
||||||
|
|
||||||
const dx = globalX - pos.x;
|
for (let x = x0; x <= x1; x++) {
|
||||||
const dy = globalY - pos.y;
|
const v = edgeGray[rowOffset + x];
|
||||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (dist > radius) {
|
if (v > EDGE_DARK_THRESHOLD) {
|
||||||
continue;
|
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));
|
if (d2 > r2) {
|
||||||
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) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
const score = Math.sqrt(d2) - (EDGE_DARK_THRESHOLD - v) * 0.03;
|
||||||
* 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 = darknessScore + gradientScore - distancePenalty;
|
if (score < bestScore) {
|
||||||
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
bestScore = score;
|
||||||
best = {
|
best = {
|
||||||
x: globalX,
|
x: x,
|
||||||
y: globalY
|
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;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1756,6 +1799,12 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dxfBtn = document.getElementById('exportDxfBtn');
|
||||||
|
|
||||||
|
if (dxfBtn) {
|
||||||
|
dxfBtn.disabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedPoint(type, holeIndex, pointIndex) {
|
function isSelectedPoint(type, holeIndex, pointIndex) {
|
||||||
@@ -2315,6 +2364,7 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('saveManualAreaBtn').disabled = false;
|
document.getElementById('saveManualAreaBtn').disabled = false;
|
||||||
|
document.getElementById('exportDxfBtn').disabled = false;
|
||||||
|
|
||||||
setManualStatus(
|
setManualStatus(
|
||||||
`Area finale: ${finalAreaMm2.toFixed(3)} mm² = ${finalAreaCm2.toFixed(4)} cm². ` +
|
`Area finale: ${finalAreaMm2.toFixed(3)} mm² = ${finalAreaCm2.toFixed(4)} cm². ` +
|
||||||
@@ -2335,8 +2385,17 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dxfContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
dxfContent = buildDxfString();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('DXF build failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id: currentManualJobId,
|
id: currentManualJobId,
|
||||||
|
dxf_content: dxfContent,
|
||||||
|
|
||||||
area_mm2: +lastManualResult.area_mm2.toFixed(6),
|
area_mm2: +lastManualResult.area_mm2.toFixed(6),
|
||||||
area_cm2: +lastManualResult.area_cm2.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) {
|
function polygonAreaPx2(points) {
|
||||||
let area = 0;
|
let area = 0;
|
||||||
|
|
||||||
@@ -2634,6 +2756,8 @@ $jobs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
btn.classList.add('btn-warning', 'active');
|
btn.classList.add('btn-warning', 'active');
|
||||||
btn.innerText = '🧲 Magnete bordo ON';
|
btn.innerText = '🧲 Magnete bordo ON';
|
||||||
|
|
||||||
|
rebuildEdgeCache();
|
||||||
|
|
||||||
setManualStatus(
|
setManualStatus(
|
||||||
'Magnete bordo attivo: quando disegni o sposti punti, il sistema prova ad agganciarli al bordo scuro più vicino.'
|
'Magnete bordo attivo: quando disegni o sposti punti, il sistema prova ad agganciarli al bordo scuro più vicino.'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ try {
|
|||||||
$outerPolygon = $input['manual_polygon'] ?? null;
|
$outerPolygon = $input['manual_polygon'] ?? null;
|
||||||
$holes = $input['manual_holes'] ?? [];
|
$holes = $input['manual_holes'] ?? [];
|
||||||
$roi = $input['roi'] ?? null;
|
$roi = $input['roi'] ?? null;
|
||||||
|
$dxfContent = isset($input['dxf_content']) ? (string)$input['dxf_content'] : '';
|
||||||
|
|
||||||
if ($areaMm2 <= 0) {
|
if ($areaMm2 <= 0) {
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
@@ -106,6 +107,43 @@ try {
|
|||||||
|
|
||||||
$manualHolesJson = json_encode($holes);
|
$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;
|
$roiX = null;
|
||||||
$roiY = null;
|
$roiY = null;
|
||||||
$roiW = null;
|
$roiW = null;
|
||||||
@@ -152,6 +190,9 @@ try {
|
|||||||
manual_holes_json = ?,
|
manual_holes_json = ?,
|
||||||
manual_status = 'completed',
|
manual_status = 'completed',
|
||||||
|
|
||||||
|
dxf_filename = COALESCE(?, dxf_filename),
|
||||||
|
dxf_url = COALESCE(?, dxf_url),
|
||||||
|
|
||||||
scale_used = ?,
|
scale_used = ?,
|
||||||
scale_detected = ?,
|
scale_detected = ?,
|
||||||
confidence = 'manual_validated',
|
confidence = 'manual_validated',
|
||||||
@@ -188,6 +229,9 @@ try {
|
|||||||
$manualPolygonJson,
|
$manualPolygonJson,
|
||||||
$manualHolesJson,
|
$manualHolesJson,
|
||||||
|
|
||||||
|
$dxfFilename,
|
||||||
|
$dxfUrl,
|
||||||
|
|
||||||
$mmPerPx,
|
$mmPerPx,
|
||||||
'manual',
|
'manual',
|
||||||
|
|
||||||
@@ -227,6 +271,9 @@ try {
|
|||||||
manual_holes_json = ?,
|
manual_holes_json = ?,
|
||||||
manual_status = 'completed',
|
manual_status = 'completed',
|
||||||
|
|
||||||
|
dxf_filename = COALESCE(?, dxf_filename),
|
||||||
|
dxf_url = COALESCE(?, dxf_url),
|
||||||
|
|
||||||
scale_used = ?,
|
scale_used = ?,
|
||||||
scale_detected = ?,
|
scale_detected = ?,
|
||||||
confidence = 'manual_validated',
|
confidence = 'manual_validated',
|
||||||
@@ -264,6 +311,9 @@ try {
|
|||||||
$manualPolygonJson,
|
$manualPolygonJson,
|
||||||
$manualHolesJson,
|
$manualHolesJson,
|
||||||
|
|
||||||
|
$dxfFilename,
|
||||||
|
$dxfUrl,
|
||||||
|
|
||||||
$mmPerPx,
|
$mmPerPx,
|
||||||
'manual',
|
'manual',
|
||||||
|
|
||||||
@@ -285,7 +335,8 @@ try {
|
|||||||
'area_mm2' => $areaMm2,
|
'area_mm2' => $areaMm2,
|
||||||
'area_cm2' => $areaCm2,
|
'area_cm2' => $areaCm2,
|
||||||
'outer_area_mm2' => $outerAreaMm2,
|
'outer_area_mm2' => $outerAreaMm2,
|
||||||
'holes_area_mm2' => $holesAreaMm2
|
'holes_area_mm2' => $holesAreaMm2,
|
||||||
|
'dxf_url' => $dxfUrl
|
||||||
]);
|
]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log('CAD manual area save error: ' . $e->getMessage());
|
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
Reference in New Issue
Block a user