import numpy as np from .. import util class HeaderError(Exception): # the exception raised if an STL file object doesn't match its header pass # define a numpy datatype for the data section of a binary STL file # everything in STL is always Little Endian # this works natively on Little Endian systems, but blows up on Big Endians # so we always specify byteorder _stl_dtype = np.dtype( [("normals", " solid_end: raise ValueError("`endsolid` precedes `solid`!") # get the chunk of text with this particular solid solid = raw_lower[solid_start:solid_end] # extract the vertices vertex_text = solid.split("vertex") vertices = np.fromstring( " ".join(line[: line.find("\n")] for line in vertex_text[1:]), sep=" ", dtype=np.float64, ) if len(vertices) < 3: continue if len(vertices) % 3 != 0: raise ValueError("incorrect number of vertices") # reshape vertices to final 3D shape vertices = vertices.reshape((-1, 3)) faces = np.arange(len(vertices)).reshape((-1, 3)) # try to extract the face normals the same way face_normals = None try: normal_text = solid.split("normal") normals = np.fromstring( " ".join(line[: line.find("\n")] for line in normal_text[1:]), sep=" ", dtype=np.float64, ) if len(normals) == len(vertices): face_normals = normals.reshape((-1, 3)) except BaseException: util.log.warning("failed to extract face_normals", exc_info=True) try: # Previously checked to make sure there was matching 'solid' for 'endsolid' # the name is right after the `solid` keyword if it exists name = raw_mixed[solid_start : solid_start + solid.find("\n")][6:].strip() except BaseException: # will be filled in by unique_name name = None # make sure geometry has a unique name for the scene name = util.unique_name(name, kwargs) # save the constructor arguments kwargs[name] = { "vertices": vertices.reshape((-1, 3)), "face_normals": face_normals, "faces": faces, "metadata": {"name": name}, } if len(kwargs) == 1: return next(iter(kwargs.values())) return {"geometry": kwargs} def export_stl(mesh) -> bytes: """ Convert a Trimesh object into a binary STL file. Parameters --------- mesh Trimesh object to export. Returns --------- export Represents mesh in binary STL form """ header = np.zeros(1, dtype=_stl_dtype_header) if hasattr(mesh, "faces"): header["face_count"] = len(mesh.faces) export = header.tobytes() if hasattr(mesh, "faces"): packed = np.zeros(len(mesh.faces), dtype=_stl_dtype) packed["normals"] = mesh.face_normals packed["vertices"] = mesh.triangles export += packed.tobytes() return export def export_stl_ascii(mesh) -> str: """ Convert a Trimesh object into an ASCII STL file. Parameters --------- mesh : trimesh.Trimesh Returns --------- export Mesh represented as an ASCII STL file """ # move all the data that's going into the STL file into one array blob = np.zeros((len(mesh.faces), 4, 3)) blob[:, 0, :] = mesh.face_normals blob[:, 1:, :] = mesh.triangles # create a lengthy format string for the data section of the file formatter = ( "\n".join( [ "facet normal {} {} {}", "outer loop", "vertex {} {} {}\nvertex {} {} {}\nvertex {} {} {}", "endloop", "endfacet", "", ] ) ) * len(mesh.faces) # try applying the name from metadata if it exists name = mesh.metadata.get("name", "") if not isinstance(name, str): name = "" if len(name) > 80 or "\n" in name: name = "" # concatenate the header, data, and footer, and a new line return "\n".join([f"solid {name}", formatter.format(*blob.reshape(-1)), "endsolid\n"]) _stl_loaders = {"stl": load_stl, "stl_ascii": load_stl}