from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import struct
import compas
from compas.geometry import Translation
from compas.utilities import geometric_key
__all__ = [
'STL',
'STLReader',
'STLParser',
'STLWriter',
]
[docs]class STL(object):
[docs] def __init__(self, filepath, precision=None):
self.filepath = filepath
self.precision = precision
self._is_parsed = False
self._reader = None
self._parser = None
self._writer = None
[docs] def read(self):
self._reader = STLReader(self.filepath)
self._parser = STLParser(self._reader, precision=self.precision)
self._is_parsed = True
[docs] def write(self, mesh, **kwargs):
self._writer = STLWriter(self.filepath, mesh, **kwargs)
self._writer.write()
@property
def reader(self):
if not self._is_parsed:
self.read()
return self._reader
@property
def parser(self):
if not self._is_parsed:
self.read()
return self._parser
[docs]class STLReader(object):
"""Standard triangle library format.
References
----------
.. [1] http://paulbourke.net/dataformats/stl/
"""
[docs] def __init__(self, filepath):
self.filepath = filepath
self.file = None
self.header = None
self.facets = []
self.read()
[docs] def read(self):
is_binary = False
with open(self.filepath, 'rb') as file:
line = file.readline().strip()
if b'solid' in line:
is_binary = False
else:
is_binary = True
try:
if not is_binary:
self.read_ascii()
else:
self.read_binary()
except Exception:
# raise if it was already detected as binary, but failed anyway
if is_binary:
raise
# else, ascii parsing failed, try binary
is_binary = True
self.read_binary()
# ==========================================================================
# ascii
#
# @see: https://en.wikipedia.org/wiki/STL_(file_format)
#
# solid name
# facet normal ni nj nk
# outer loop
# vertex v1x v1y v1z
# vertex v2x v2y v2z
# vertex v3x v3y v3z
# endloop
# endfacet
# endsolid name
#
# ==========================================================================
[docs] def read_ascii(self):
with open(self.filepath, 'r') as file:
self.file = file
self.file.seek(0)
self.facets = self.read_solids_ascii()
[docs] def read_solids_ascii(self):
solids = {}
facets = []
while True:
line = self.file.readline().strip()
if not line:
break
parts = line.split()
if parts[0] == 'solid':
if len(parts) == 2:
name = parts[1]
else:
name = 'solid'
solids[name] = []
elif parts[0] == 'endsolid':
name = None
elif parts[0] == 'facet':
facet = {'normal': None, 'vertices': None}
if parts[1] == 'normal':
facet['normal'] = [float(parts[i]) for i in range(2, 5)]
elif parts[0] == 'outer' and parts[1] == 'loop':
vertices = []
elif parts[0] == 'vertex':
xyz = [float(parts[i]) for i in range(1, 4)]
vertices.append(xyz)
elif parts[0] == 'endloop':
facet['vertices'] = vertices
elif parts[0] == 'endfacet':
solids[name].append(facet)
facets.append(facet)
# no known line start matches, maybe not ascii
elif not parts[0].isalnum():
raise RuntimeError('File is not ASCII')
return facets
# ==========================================================================
# binary
#
# @see: https://en.wikipedia.org/wiki/STL_(file_format)
#
# UINT8[80] - Header
# UINT32 - Number of triangles
#
# foreach triangle
# REAL32[3] - Normal vector
# REAL32[3] - Vertex 1
# REAL32[3] - Vertex 2
# REAL32[3] - Vertex 3
# UINT16 - Attribute byte count
# end
#
# ==========================================================================
def _read_uint16(self):
bytes_ = self.file.read(2)
return struct.unpack('<H', bytes_)[0]
def _read_uint32(self):
bytes_ = self.file.read(4)
return struct.unpack('<I', bytes_)[0]
[docs] def read_binary(self):
with open(self.filepath, 'rb') as file:
self.file = file
self.file.seek(0)
self.header = self.read_header_binary()
self.facets = self.read_facets_binary()
[docs] def read_number_of_facets_binary(self):
return self._read_uint32()
[docs] def read_facet_binary(self):
# Read full facet at once
# 4 bytes per float * 3 floats per vector/vertex * 4 items (1 vector + 3 vertices)
bytes_ = self.file.read(48)
floats_ = struct.unpack('<12f', bytes_)
normal = floats_[0:3]
vertices = (floats_[3:6], floats_[6:9], floats_[9:12])
keys = (bytes_[12:24], bytes_[24:36], bytes_[36:48])
# Skip two-byte attributes, it's not used anywhere (by anyone - on this planet)
self.file.seek(2, 1)
return {'normal': normal, 'vertices': vertices, 'keys': keys}
[docs] def read_facets_binary(self):
facets = []
n = self.read_number_of_facets_binary()
for i in range(n):
facets.append(self.read_facet_binary())
return facets
[docs]class STLParser(object):
""""""
[docs] def __init__(self, reader, precision=None):
self.precision = precision
self.reader = reader
self.vertices = None
self.faces = None
self.parse()
[docs] def parse(self):
gkey_index = {}
vertices = []
faces = []
for facet in self.reader.facets:
face = []
facet_vertices = facet['vertices']
for i in range(3):
xyz = facet_vertices[i]
if 'keys' in facet:
gkey = facet['keys'][i]
else:
gkey = geometric_key(xyz, self.precision)
if gkey not in gkey_index:
gkey_index[gkey] = len(vertices)
vertices.append(xyz)
face.append(gkey_index[gkey])
faces.append(face)
self.vertices = vertices
self.faces = faces
[docs]class STLWriter(object):
""""""
[docs] def __init__(self, filepath, mesh, binary=False, solid_name=None, precision=None):
self.filepath = filepath
self.mesh = mesh
self.solid_name = solid_name or mesh.name
self.precision = precision or compas.PRECISION
self.file = None
self.binary = binary
@property
def vertex_xyz(self):
bbox = self.mesh.bounding_box()
xmin, ymin, zmin = bbox[0]
if not self.binary and (xmin < 0 or ymin < 0 or zmin < 0):
T = Translation.from_vector([-xmin, -ymin, -zmin])
mesh = self.mesh.transformed(T)
else:
mesh = self.mesh
return {vertex: mesh.vertex_attributes(vertex, 'xyz') for vertex in mesh.vertices()}
[docs] def write(self):
if not self.mesh.is_trimesh():
raise ValueError('Mesh must be triangular to be encoded in STL.')
if not self.binary:
with open(self.filepath, 'w') as self.file:
self.write_header()
self.write_faces()
self.write_footer()
else:
with open(self.filepath, 'wb') as self.file:
self.file.seek(0)
self.write_binary_header()
self.write_binary_num_faces()
self.write_binary_faces()
[docs] def write_faces(self):
vertex_xyz = self.vertex_xyz
for face in self.mesh.faces():
self.file.write("facet normal {0} {1} {2}\n".format(* self.mesh.face_normal(face)))
self.file.write(" outer loop\n")
for vertex in self.mesh.face_vertices(face):
self.file.write(" vertex {0} {1} {2}\n".format(* vertex_xyz[vertex]))
self.file.write(" endloop\n")
self.file.write("endfacet\n")
[docs] def write_binary_num_faces(self):
try:
self.file.write(struct.pack('<L', self.mesh.number_of_faces()))
except struct.error:
raise ValueError('Mesh must have fewer than 4294967295 faces to be written to binary STL.')
[docs] def write_binary_faces(self):
vertex_xyz = self.vertex_xyz
for face in self.mesh.faces():
self.file.write(struct.pack('<3f', *self.mesh.face_normal(face)))
for vertex in self.mesh.face_vertices(face):
self.file.write(struct.pack('<3f', *vertex_xyz[vertex]))
self.file.write(b'\0\0')
# ==============================================================================
# Main
# ==============================================================================
if __name__ == "__main__":
import doctest
doctest.testmod(globs=globals())
# import os
# from compas.datastructures import Mesh
# from compas_viewers.meshviewer import MeshViewer
# from compas.utilities import download_file_from_remote
# from compas.topology import connected_components
# source = 'https://raw.githubusercontent.com/ros-industrial/abb/kinetic-devel/abb_irb6600_support/meshes/irb6640/visual/link_1.stl'
# filepath = os.path.join(compas.APPDATA, 'data', 'meshes', 'ros', 'link_1.stl')
# download_file_from_remote(source, filepath, overwrite=False)
# stl = STL(filepath, precision='6f')
# mesh = Mesh.from_vertices_and_faces(stl.parser.vertices, stl.parser.faces)
# vertexgroups = connected_components(mesh.halfedge)
# facegroups = [[] for _ in range(len(vertexgroups))]
# vertexsets = list(map(set, vertexgroups))
# for fkey in mesh.faces():
# vertices = set(mesh.face_vertices(fkey))
# for i, vertexset in enumerate(vertexsets):
# if vertices.issubset(vertexset):
# facegroups[i].append(fkey)
# break
# meshes = []
# for vertexgroup, facegroup in zip(vertexgroups, facegroups):
# key_index = {key: index for index, key in enumerate(vertexgroup)}
# vertices = mesh.vertices_attributes('xyz', keys=vertexgroup)
# faces = [[key_index[key] for key in mesh.face_vertices(fkey)] for fkey in facegroup]
# meshes.append(Mesh.from_vertices_and_faces(vertices, faces))
# viewer = MeshViewer()
# viewer.mesh = meshes[0]
# viewer.show()