vision: anteprima DXF nel worker e nel backend (Refs #101)
Worker: POST /dxf/preview legge il DXF con read_xld_dxf (lo stesso lettore di CompareDxf) e ritorna contorni in mm con y verso il basso, bbox, dimensioni, entita e punti (tetto 20000, decimazione uniforme). Backend: POST /api/vision/dxf-preview (Maker) che inoltra al worker; 422 inoltrato. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile # #101
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
@@ -288,3 +289,89 @@ async def recipes_dxf_compare(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"recipe build failed: {type(exc).__name__}: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
# #101: tetto ai punti dell'anteprima DXF (un profilo fitto non deve gonfiare
|
||||
# la risposta ne' rallentare il disegno SVG nel browser).
|
||||
_PREVIEW_MAX_POINTS = 20000
|
||||
|
||||
|
||||
def _decimate(points: list[list[float]], keep: int) -> list[list[float]]:
|
||||
"""Decimazione uniforme a `keep` punti (>= 2), primo e ultimo sempre inclusi."""
|
||||
n = len(points)
|
||||
if n <= keep or keep < 2:
|
||||
return points
|
||||
idx = sorted({round(i * (n - 1) / (keep - 1)) for i in range(keep)})
|
||||
return [points[i] for i in idx]
|
||||
|
||||
|
||||
def _dxf_preview(dxf_bytes: bytes) -> dict:
|
||||
"""#101: legge il DXF con lo stesso lettore di CompareDxf (read_xld_dxf) e ne
|
||||
ritorna i contorni in mm, asse y verso il basso (come immagine e SVG).
|
||||
|
||||
Funzione sincrona e pura: la rotta la esegue in un thread. Solleva
|
||||
CostruttoreError per DXF vuoto, illeggibile o senza geometria.
|
||||
"""
|
||||
if not dxf_bytes:
|
||||
raise CostruttoreError("DXF illeggibile: file vuoto")
|
||||
from visionsuite.io.dxf import read_xld_dxf
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "disegno.dxf"
|
||||
path.write_bytes(dxf_bytes)
|
||||
try:
|
||||
# flip_y=False + scale=1: coordinate native del DXF, in mm; il
|
||||
# ribaltamento dell'asse y lo facciamo noi sotto (y = -y_dxf)
|
||||
conts = list(read_xld_dxf(str(path), sampling_distance=0.05,
|
||||
scale=1.0, flip_y=False))
|
||||
except Exception as exc: # noqa: BLE001 - qualunque errore di lettura = DXF illeggibile
|
||||
raise CostruttoreError(f"DXF illeggibile: {type(exc).__name__}: {exc}") from exc
|
||||
|
||||
contorni: list[list[list[float]]] = []
|
||||
for cont in conts:
|
||||
pts = np.asarray(cont.points, dtype=float) # (riga, colonna) = (y_dxf, x_dxf)
|
||||
if pts.ndim != 2 or pts.shape[0] < 2:
|
||||
continue
|
||||
line = [[float(c), float(-r)] for r, c in pts]
|
||||
if cont.closed and line[0] != line[-1]:
|
||||
line.append(list(line[0])) # chiude il giro anche nella polilinea SVG
|
||||
contorni.append(line)
|
||||
if not contorni:
|
||||
raise CostruttoreError("DXF senza geometria")
|
||||
|
||||
# #101: le dimensioni si misurano sui punti completi, prima della decimazione
|
||||
xs = [p[0] for c in contorni for p in c]
|
||||
ys = [p[1] for c in contorni for p in c]
|
||||
xmin, xmax, ymin, ymax = min(xs), max(xs), min(ys), max(ys)
|
||||
|
||||
total = len(xs)
|
||||
if total > _PREVIEW_MAX_POINTS:
|
||||
# quota per contorno proporzionale, minimo 2 punti
|
||||
contorni = [
|
||||
_decimate(c, max(2, int(len(c) * _PREVIEW_MAX_POINTS / total))) for c in contorni
|
||||
]
|
||||
contorni = [[[round(x, 3), round(y, 3)] for x, y in c] for c in contorni]
|
||||
xmin, xmax, ymin, ymax = (round(v, 3) for v in (xmin, xmax, ymin, ymax))
|
||||
return {
|
||||
"contorni": contorni,
|
||||
"bbox": {"xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax},
|
||||
"larghezza_mm": round(xmax - xmin, 3),
|
||||
"altezza_mm": round(ymax - ymin, 3),
|
||||
"entita": len(contorni),
|
||||
"punti": sum(len(c) for c in contorni),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/dxf/preview")
|
||||
async def dxf_preview(dxf: UploadFile = File(...)) -> dict:
|
||||
"""#101: anteprima del disegno DXF (contorni + dimensioni in mm) per il
|
||||
wizard «Crea ricetta da DXF». Solo lettura, leggera: nessun run slot."""
|
||||
dxf_bytes = await dxf.read()
|
||||
try:
|
||||
return await asyncio.to_thread(_dxf_preview, dxf_bytes)
|
||||
except CostruttoreError as exc:
|
||||
raise _unprocessable(str(exc)) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"dxf preview failed: {type(exc).__name__}: {exc}",
|
||||
) from exc
|
||||
|
||||
Reference in New Issue
Block a user