Files
pdm/creoparams/mesh.py
T

128 lines
4.2 KiB
Python
Raw Normal View History

"""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("<I", data[80:84])[0]
return len(data) == BINARY_HEADER + count * TRIANGLE_SIZE
def _read_binary(data: bytes) -> list:
count = struct.unpack("<I", data[80:84])[0]
triangles = []
offset = BINARY_HEADER
for _ in range(count):
values = struct.unpack("<12f", data[offset:offset + 48])
triangles.append((values[3:6], values[6:9], values[9:12]))
offset += TRIANGLE_SIZE
return triangles
def _read_ascii(data: bytes) -> 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)