128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
"""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)
|