Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -184,6 +184,66 @@ def _candidate_anchors(gray: np.ndarray, dxf_path: Path, px_per_mm: float,
|
|||||||
return anchors
|
return anchors
|
||||||
|
|
||||||
|
|
||||||
|
def chiudi_contorni(dxf_bytes: bytes) -> tuple[bytes, dict]:
|
||||||
|
"""#103: unisce le entita' aperte (LINE/ARC/polilinee aperte in fila) in contorni chiusi.
|
||||||
|
|
||||||
|
CompareDxf taglia il contorno misurato agli estremi di ogni entita' aperta
|
||||||
|
del disegno (cut_open_ends): con un profilo spezzato in LINE+ARC e banda
|
||||||
|
larga non resta nessun punto in banda e il confronto fallisce. Qui le catene
|
||||||
|
chiuse diventano una LWPOLYLINE chiusa (con bulge per gli archi).
|
||||||
|
|
||||||
|
Pura e senza eccezioni nuove: se non c'e' nulla da unire, o ezdxf non legge
|
||||||
|
il file, ritorna i byte originali (sha invariato, ricette esistenti stabili).
|
||||||
|
Ritorna (bytes, {"anelli", "entita_unite", "entita_libere"}).
|
||||||
|
"""
|
||||||
|
nulla = {"anelli": 0, "entita_unite": 0, "entita_libere": 0}
|
||||||
|
try:
|
||||||
|
import ezdxf
|
||||||
|
from ezdxf import edgeminer as em, edgesmith as es
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src = Path(tmp) / "in.dxf"
|
||||||
|
src.write_bytes(dxf_bytes)
|
||||||
|
doc = ezdxf.readfile(str(src))
|
||||||
|
msp = doc.modelspace()
|
||||||
|
# edges_from_entities_2d salta le entita' gia' chiuse (restano dove sono)
|
||||||
|
edges = list(es.edges_from_entities_2d(msp))
|
||||||
|
if not edges:
|
||||||
|
return dxf_bytes, nulla
|
||||||
|
loops = em.find_all_loops(em.Deposit(edges, gap_tol=0.01)) # 0.01 mm: giunzioni CAD
|
||||||
|
usati: set = set()
|
||||||
|
unite: list = [] # (polilinea, entita' di partenza)
|
||||||
|
# loop piu' lungo per primo, ogni edge usato una sola volta
|
||||||
|
for loop in sorted(loops, key=lambda lp: -sum(e.length for e in lp)):
|
||||||
|
ids = {e.id for e in loop}
|
||||||
|
if ids & usati:
|
||||||
|
continue
|
||||||
|
usati |= ids
|
||||||
|
unite.append((loop, es.lwpolyline_from_chain(loop)))
|
||||||
|
if not unite:
|
||||||
|
return dxf_bytes, {**nulla, "entita_libere": len(edges)}
|
||||||
|
entita_unite = 0
|
||||||
|
for loop, pl in unite:
|
||||||
|
pl.closed = True
|
||||||
|
payload = getattr(loop[0], "payload", None)
|
||||||
|
layer = getattr(getattr(payload, "dxf", None), "layer", None)
|
||||||
|
if layer:
|
||||||
|
pl.dxf.layer = layer # stesso layer dell'entita' di partenza
|
||||||
|
for e in loop:
|
||||||
|
ent = getattr(e, "payload", None)
|
||||||
|
if ent is not None and ent.is_alive:
|
||||||
|
msp.delete_entity(ent) # sostituita dalla polilinea chiusa
|
||||||
|
msp.add_foreign_entity(pl)
|
||||||
|
entita_unite += len(loop)
|
||||||
|
dst = Path(tmp) / "out.dxf"
|
||||||
|
doc.saveas(str(dst))
|
||||||
|
info = {"anelli": len(unite), "entita_unite": entita_unite,
|
||||||
|
"entita_libere": len(edges) - entita_unite}
|
||||||
|
return dst.read_bytes(), info
|
||||||
|
except Exception: # noqa: BLE001 - DXF illeggibile: l'errore resta quello a valle
|
||||||
|
return dxf_bytes, nulla
|
||||||
|
|
||||||
|
|
||||||
def _run(gray, graph, px_per_mm, folder) -> RunOutcome:
|
def _run(gray, graph, px_per_mm, folder) -> RunOutcome:
|
||||||
return run_graph(gray, graph, calibration={"px_per_mm": px_per_mm}, recipe_dir=folder)
|
return run_graph(gray, graph, calibration={"px_per_mm": px_per_mm}, recipe_dir=folder)
|
||||||
|
|
||||||
@@ -211,6 +271,9 @@ def build_dxf_compare(
|
|||||||
raise CostruttoreError(f"polarity deve essere una di {list(_POLARITIES)}")
|
raise CostruttoreError(f"polarity deve essere una di {list(_POLARITIES)}")
|
||||||
if not dxf_bytes:
|
if not dxf_bytes:
|
||||||
raise CostruttoreError("DXF vuoto")
|
raise CostruttoreError("DXF vuoto")
|
||||||
|
# #103: entita' aperte unite in contorni chiusi PRIMA dello sha: disegno.dxf,
|
||||||
|
# grafo e runtime usano tutti il DXF chiuso
|
||||||
|
dxf_bytes, _info_chiusura = chiudi_contorni(dxf_bytes)
|
||||||
|
|
||||||
frame = _gray_u8(gray)
|
frame = _gray_u8(gray)
|
||||||
calibration_bytes = json.dumps({"px_per_mm": px_per_mm}).encode("utf-8")
|
calibration_bytes = json.dumps({"px_per_mm": px_per_mm}).encode("utf-8")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import numpy as np
|
|||||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, status
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, status
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
|
||||||
from src.vision.costruttori import CostruttoreError, build_dxf_compare # #83
|
from src.vision.costruttori import CostruttoreError, build_dxf_compare, chiudi_contorni # #83 #103
|
||||||
from src.vision.runner import (
|
from src.vision.runner import (
|
||||||
GraphBuildError,
|
GraphBuildError,
|
||||||
RunOutcome,
|
RunOutcome,
|
||||||
@@ -315,6 +315,8 @@ def _dxf_preview(dxf_bytes: bytes) -> dict:
|
|||||||
if not dxf_bytes:
|
if not dxf_bytes:
|
||||||
raise CostruttoreError("DXF illeggibile: file vuoto")
|
raise CostruttoreError("DXF illeggibile: file vuoto")
|
||||||
from visionsuite.io.dxf import read_xld_dxf
|
from visionsuite.io.dxf import read_xld_dxf
|
||||||
|
# #103: l'anteprima mostra cio' che il confronto usera' (contorni chiusi)
|
||||||
|
dxf_bytes, _ = chiudi_contorni(dxf_bytes)
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
path = Path(tmp) / "disegno.dxf"
|
path = Path(tmp) / "disegno.dxf"
|
||||||
path.write_bytes(dxf_bytes)
|
path.write_bytes(dxf_bytes)
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""#103: chiudi_contorni unisce LINE/ARC aperti in contorni chiusi (test sincroni, no pytest-asyncio)."""
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import ezdxf
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.vision.costruttori import CostruttoreError, build_dxf_compare, chiudi_contorni
|
||||||
|
from src.vision_worker.main import _dxf_preview
|
||||||
|
|
||||||
|
|
||||||
|
def _dxf(riempi) -> bytes:
|
||||||
|
doc = ezdxf.new("R2010")
|
||||||
|
riempi(doc.modelspace())
|
||||||
|
buf = io.StringIO()
|
||||||
|
doc.write(buf)
|
||||||
|
return buf.getvalue().encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _quattro_line(msp, w=20.0, h=10.0):
|
||||||
|
pts = [(0, 0), (w, 0), (w, h), (0, h)]
|
||||||
|
for i in range(4):
|
||||||
|
msp.add_line(pts[i], pts[(i + 1) % 4])
|
||||||
|
|
||||||
|
|
||||||
|
def _entita(dxf: bytes):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp: # readfile gestisce da solo codifica e CRLF
|
||||||
|
path = Path(tmp) / "o.dxf"
|
||||||
|
path.write_bytes(dxf)
|
||||||
|
return list(ezdxf.readfile(str(path)).modelspace())
|
||||||
|
|
||||||
|
|
||||||
|
def test_chiudi_rettangolo_di_4_line_in_una_polilinea_chiusa():
|
||||||
|
out, info = chiudi_contorni(_dxf(_quattro_line))
|
||||||
|
assert info == {"anelli": 1, "entita_unite": 4, "entita_libere": 0}
|
||||||
|
ents = _entita(out)
|
||||||
|
assert [e.dxftype() for e in ents] == ["LWPOLYLINE"] and ents[0].closed
|
||||||
|
xs = [p[0] for p in ents[0].get_points()]
|
||||||
|
ys = [p[1] for p in ents[0].get_points()]
|
||||||
|
assert (min(xs), max(xs), min(ys), max(ys)) == (0, 20, 0, 10) # bbox invariato
|
||||||
|
|
||||||
|
|
||||||
|
def test_chiudi_asola_line_e_arc_ha_bulge():
|
||||||
|
def asola(msp): # asola 20x10: due LINE orizzontali + due semicerchi r=5
|
||||||
|
msp.add_line((5, 0), (15, 0))
|
||||||
|
msp.add_arc((15, 5), 5, -90, 90)
|
||||||
|
msp.add_line((15, 10), (5, 10))
|
||||||
|
msp.add_arc((5, 5), 5, 90, 270)
|
||||||
|
out, info = chiudi_contorni(_dxf(asola))
|
||||||
|
assert info["anelli"] == 1 and info["entita_unite"] == 4
|
||||||
|
ents = _entita(out)
|
||||||
|
assert len(ents) == 1 and ents[0].closed
|
||||||
|
assert any(abs(p[4]) > 1e-6 for p in ents[0].get_points()) # gli archi diventano bulge
|
||||||
|
|
||||||
|
|
||||||
|
def test_dxf_gia_chiuso_resta_identico():
|
||||||
|
def chiuso(msp):
|
||||||
|
msp.add_lwpolyline([(0, 0), (20, 0), (20, 10), (0, 10)], close=True)
|
||||||
|
msp.add_circle((10, 5), 2)
|
||||||
|
dxf = _dxf(chiuso)
|
||||||
|
out, info = chiudi_contorni(dxf)
|
||||||
|
assert out == dxf and info["anelli"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_line_isolata_resta_e_byte_identici():
|
||||||
|
dxf = _dxf(lambda msp: msp.add_line((0, 0), (10, 0)))
|
||||||
|
out, info = chiudi_contorni(dxf)
|
||||||
|
assert out == dxf and info["anelli"] == 0 and info["entita_libere"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_byte_non_dxf_ritornati_identici():
|
||||||
|
brutto = b"questo non e' un dxf"
|
||||||
|
out, info = chiudi_contorni(brutto)
|
||||||
|
assert out == brutto and info["anelli"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_preview_di_quattro_line_e_una_sola_entita():
|
||||||
|
resp = _dxf_preview(_dxf(_quattro_line))
|
||||||
|
assert resp["entita"] == 1 and resp["larghezza_mm"] == 20 and resp["altezza_mm"] == 10
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("band", [0.1, 0.3])
|
||||||
|
def test_chiudi_build_dxf_compare_banda_larga_con_4_line(band):
|
||||||
|
# rettangolo scuro 20x10 mm su bianco, 20 px/mm; disegno = 4 LINE aperte
|
||||||
|
ppm = 20.0
|
||||||
|
img = np.full((int(40 * ppm), int(60 * ppm)), 255, np.uint8)
|
||||||
|
cx, cy = img.shape[1] // 2, img.shape[0] // 2
|
||||||
|
cv2.rectangle(img, (cx - int(10 * ppm), cy - int(5 * ppm)),
|
||||||
|
(cx + int(10 * ppm), cy + int(5 * ppm)), 0, -1)
|
||||||
|
dxf = _dxf(_quattro_line)
|
||||||
|
ris = build_dxf_compare(img, dxf, px_per_mm=ppm, band_mm=band)
|
||||||
|
assert ris["outputs"]["dxf.points_in_pct"] > 90.0
|
||||||
|
# il disegno nella ricetta e' quello chiuso, non l'originale
|
||||||
|
import base64
|
||||||
|
salvato = base64.b64decode(ris["files"]["disegno.dxf"])
|
||||||
|
assert salvato != dxf and hashlib.sha256(salvato).hexdigest() == \
|
||||||
|
ris["graph"]["meta"]["dxf"]["disegno"]["sha256"]
|
||||||
Reference in New Issue
Block a user