La prima versione era poco leggibile: l'ombreggiatura usava solo l'inclinazione frontale, quindi le facce complanari di un pezzo piatto uscivano tutte dello stesso grigio e la forma non si distingueva. Due correzioni: - luce direzionale obliqua invece che frontale, con una rampa di luminosita' piu' ampia, cosi' le facce orientate diversamente si separano davvero; - soprattutto, gli spigoli. Sopra alle superfici si disegnano ora gli spigoli vivi (due facce che formano un angolo netto) e la silhouette (faccia visibile contro faccia nascosta). Sono le linee che rendono riconoscibile un pezzo meccanico, le stesse che traccerebbe un disegnatore. Per distinguere uno spigolo vivo da una suddivisione interna della tassellatura servono le facce adiacenti, che un STL non fornisce perche' ripete i vertici a ogni triangolo. Mesh.indexed() li fonde e ricostruisce la topologia: sul pezzo di prova 912 ripetizioni diventano 144 vertici reali, 304 facce e 456 spigoli, senza bordi liberi. La mesh risulta chiusa e la caratteristica di Eulero indica cinque fori passanti, come il pezzo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
160 lines
5.5 KiB
Python
160 lines
5.5 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 indexed(self, tolerance: int = 5):
|
|
"""Vertici unici, facce e spigoli, con le facce adiacenti a ciascuno.
|
|
|
|
Un STL ripete i vertici a ogni triangolo: fondendoli possiamo sapere
|
|
quali facce condividono uno spigolo, e quindi distinguere gli spigoli
|
|
vivi del pezzo dalle suddivisioni interne della tassellatura. Sono
|
|
quelli che rendono leggibile un modello meccanico.
|
|
"""
|
|
lookup, vertices, faces = {}, [], []
|
|
for triangle in self.triangles:
|
|
face = []
|
|
for vertex in triangle:
|
|
key = tuple(round(c, tolerance) for c in vertex)
|
|
index = lookup.get(key)
|
|
if index is None:
|
|
index = len(vertices)
|
|
lookup[key] = index
|
|
vertices.append([round(c, 4) for c in vertex])
|
|
face.append(index)
|
|
if len(set(face)) == 3: # scarta i triangoli degeneri
|
|
faces.append(face)
|
|
|
|
edges = {}
|
|
for number, (a, b, c) in enumerate(faces):
|
|
for pair in ((a, b), (b, c), (c, a)):
|
|
key = (min(pair), max(pair))
|
|
edges.setdefault(key, []).append(number)
|
|
|
|
edge_list = [[a, b, adj[0], adj[1] if len(adj) > 1 else -1]
|
|
for (a, b), adj in edges.items()]
|
|
return vertices, faces, edge_list
|
|
|
|
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)
|