"""Lettura di mesh triangolari (STL) e calcolo delle grandezze derivate. La geometria dei `.prt` e' scritta nel formato del kernel Granite di PTC e non e' leggibile senza la libreria di PTC: verificato cercando nel file i 22 valori esatti del modello (raggi e coordinate presi da un export STEP) in cinque codifiche diverse, senza un solo riscontro. La forma del pezzo arriva quindi da una mesh esportata da Creo. Questo modulo la legge; la fonte e' sostituibile senza toccare il resto (per esempio con il Granite Interoperability Kernel, che legge i .prt nativi senza Creo installato). """ from __future__ import annotations import math import struct from dataclasses import dataclass BINARY_HEADER = 84 TRIANGLE_SIZE = 50 @dataclass class Mesh: """Triangoli come terne di vertici (x, y, z).""" triangles: list source: str = "" def __len__(self): return len(self.triangles) @property def bounds(self): xs = [v[0] for t in self.triangles for v in t] ys = [v[1] for t in self.triangles for v in t] zs = [v[2] for t in self.triangles for v in t] return (min(xs), min(ys), min(zs)), (max(xs), max(ys), max(zs)) @property def size(self): lo, hi = self.bounds return tuple(round(hi[i] - lo[i], 4) for i in range(3)) @property def center(self): lo, hi = self.bounds return tuple((hi[i] + lo[i]) / 2 for i in range(3)) def area(self) -> float: """Area della superficie, in unita' del modello al quadrato.""" total = 0.0 for a, b, c in self.triangles: ux, uy, uz = b[0]-a[0], b[1]-a[1], b[2]-a[2] vx, vy, vz = c[0]-a[0], c[1]-a[1], c[2]-a[2] nx, ny, nz = uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx total += math.sqrt(nx*nx + ny*ny + nz*nz) / 2 return total def volume(self) -> float: """Volume col metodo dei tetraedri con segno. Vale solo se la mesh e' chiusa: su una mesh aperta il risultato non ha senso.""" total = 0.0 for a, b, c in self.triangles: total += (a[0]*(b[1]*c[2]-b[2]*c[1]) - a[1]*(b[0]*c[2]-b[2]*c[0]) + a[2]*(b[0]*c[1]-b[1]*c[0])) / 6 return abs(total) def to_dict(self) -> dict: lo, hi = self.bounds return { "source": self.source, "triangles": len(self.triangles), "size": self.size, "bounds": {"min": [round(v, 4) for v in lo], "max": [round(v, 4) for v in hi]}, "area": round(self.area(), 3), "volume": round(self.volume(), 3), } def _is_binary(data: bytes) -> bool: """Un STL ASCII inizia con 'solid', ma anche molti binari lo fanno: l'unico controllo affidabile e' la dimensione attesa.""" if len(data) < BINARY_HEADER: return False count = struct.unpack(" list: count = struct.unpack(" list: triangles, current = [], [] for line in data.decode("ascii", errors="replace").splitlines(): parts = line.split() if len(parts) == 4 and parts[0] == "vertex": try: current.append(tuple(float(v) for v in parts[1:])) except ValueError: current = [] if len(current) == 3: triangles.append(tuple(current)) current = [] elif parts and parts[0] == "facet": current = [] return triangles def read_stl(path: str) -> Mesh: with open(path, "rb") as handle: data = handle.read() triangles = _read_binary(data) if _is_binary(data) else _read_ascii(data) if not triangles: raise ValueError("nessun triangolo trovato: il file non e' un STL valido") return Mesh(triangles, source=path)