#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
STL reader & writer.
https://en.wikipedia.org/wiki/STL_(file_format)
"""
import struct
import warnings
from typing import List
from binaryornot.check import is_binary
from kaitaistruct import KaitaiStream
import design3d as d3d
import design3d.core as d3dc
import design3d.faces as d3df
from design3d import shells
[docs]
class Stl:
"""
STL files are used to represent simple 3D models, defined using triangular 3D faces.
Initially it was introduced as native format for 3D Systems
Stereo-lithography CAD system, but due to its extreme simplicity, it
was adopted by a wide range of 3D modeling, CAD, rapid prototyping
and 3D printing applications as the simplest 3D model exchange
format.
STL is extremely bare-bones format: there are no complex headers, no
texture / color support, no units specifications, no distinct vertex
arrays. Whole model is specified as a collection of triangular
faces.
There are two versions of the format (text and binary), this spec
describes binary version.
"""
_standalone_in_db = True
_dessia_methods = ['from_text_stream', 'from_text_stream', 'to_closed_shell', 'to_open_shell']
def __init__(self, triangles: List[d3df.Triangle3D], name: str = ''):
warnings.warn(
"'design3d.stl.Stl' class is deprecated. Use 'design3d.display.Mesh3D' instead",
DeprecationWarning
)
self.triangles = triangles
self.name=name
self.normals = None
[docs]
@classmethod
def points_from_file(cls, filename: str, distance_multiplier=0.001):
"""
Read points from an STL file and return a list of points.
:param filename: The path to the STL file.
:type filename: str
:param distance_multiplier: (optional) The distance multiplier. Defaults to 0.001.
:type distance_multiplier: float
:return: A list of Point3D objects.
:rtype: List[d3d.Point3D]
"""
if is_binary(filename):
with open(filename, 'rb') as file:
stream = KaitaiStream(file)
_ = stream.read_bytes(80).decode('utf8')
num_triangles = stream.read_u4le()
all_points = []
for i in range(num_triangles):
if i % 5000 == 0:
print('reading stl',
round(i / num_triangles * 100, 2), '%')
# First is normal, unused
_ = d3d.Vector3D(stream.read_f4le(),
stream.read_f4le(),
stream.read_f4le())
p1 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
p2 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
p3 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
all_points.extend([p1, p2, p3])
stream.read_u2le()
return all_points
[docs]
@classmethod
def from_binary_stream(cls, stream, distance_multiplier: float = 0.001):
"""
Create an STL object from a binary stream.
:param stream: The binary stream containing the STL data.
:type stream: BinaryFile
:param distance_multiplier: (optional) The distance multiplier. Defaults to 0.001.
:type distance_multiplier: float
:return: An instance of the Stl class.
:rtype: Stl
"""
stream.seek(0)
stream = KaitaiStream(stream)
name_slice = stream.read_bytes(80)
try:
name = name_slice.decode('utf-8')
except UnicodeDecodeError:
name = name_slice.decode('latin-1')
num_triangles = stream.read_u4le()
# print(num_triangles)
triangles = [None] * num_triangles
invalid_triangles = []
for i in range(num_triangles):
if i % 5000 == 0:
print('reading stl',
round(i / num_triangles * 100, 2), '%')
_ = d3d.Vector3D(stream.read_f4le(),
stream.read_f4le(),
stream.read_f4le())
p1 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
p2 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
p3 = d3d.Point3D(distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le(),
distance_multiplier * stream.read_f4le())
try:
triangles[i] = d3df.Triangle3D(p1, p2, p3)
except ZeroDivisionError:
invalid_triangles.append(i)
except NotImplementedError:
invalid_triangles.append(i)
stream.read_u2le()
# print(abr)
if invalid_triangles:
# print('invalid_triangles number: ', len(invalid_triangles))
for i in invalid_triangles[::-1]:
del triangles[i]
return cls(triangles, name=name)
[docs]
@classmethod
def from_text_stream(cls, stream,
distance_multiplier: float = 0.001):
"""
Create an STL object from a text stream.
:param stream: The text stream containing the STL data.
:type stream: StringFile
:param distance_multiplier: (optional) The distance multiplier. Defaults to 0.001.
:type distance_multiplier: float
:return: An instance of the Stl class.
:rtype: Stl
"""
stream.seek(0)
header = stream.readline()
name = header[6:]
triangles = []
points = []
for line in stream.readlines():
if 'vertex' in line:
line = line.replace('vertex', '')
line = line.lstrip(' ')
x, y, z = [i for i in line.split(' ') if i]
points.append(d3d.Point3D(distance_multiplier * float(x),
distance_multiplier * float(y),
distance_multiplier * float(z)))
if 'endfacet' in line:
try:
triangles.append(d3df.Triangle3D(points[0],
points[1],
points[2]))
except (ZeroDivisionError, NotImplementedError): # NotImplementedError comes from equal points
pass
points = []
return cls(triangles, name=name)
[docs]
@classmethod
def from_file(cls, filename: str = None,
distance_multiplier: float = 0.001):
"""Import stl from file."""
warnings.warn("Use load_from_file instead of from_file",
DeprecationWarning)
return cls.load_from_file(filename, distance_multiplier)
[docs]
@classmethod
def load_from_file(cls, filepath: str, distance_multiplier: float = 0.001):
"""
Load an STL object from a file.
:param filepath: The path to the STL file.
:type filepath: str
:param distance_multiplier: (optional) The distance multiplier. Defaults to 0.001.
:type distance_multiplier: float
:return: An instance of the Stl class.
:rtype: Stl
"""
if is_binary(filepath):
with open(filepath, 'rb') as file:
return cls.from_binary_stream(
file, distance_multiplier=distance_multiplier)
with open(filepath, 'r', encoding='utf-8', errors='ignore') as file:
return cls.from_text_stream(
file, distance_multiplier=distance_multiplier)
[docs]
def save_to_binary_file(self, filepath, distance_multiplier=1000):
"""
Save the STL object into a binary file.
:param filepath: The path to the STL file.
:type filepath: str
:param distance_multiplier: (optional) The distance multiplier. Defaults to 1000.
:type distance_multiplier: float
:return: An instance of the Stl class.
:rtype: Stl
"""
if not filepath.endswith('.stl'):
filepath += '.stl'
print('Adding .stl extension: ', filepath)
with open(filepath, 'wb') as file:
self.save_to_stream(file, distance_multiplier=distance_multiplier)
[docs]
def save_to_stream(self, stream, distance_multiplier=1000):
"""
Save the STL object into a binary file.
:param stream: The binary stream containing the STL data.
:type filepath: BinaryFile
:param distance_multiplier: (optional) The distance multiplier. Defaults to 1000.
:type distance_multiplier: float
:return: An instance of the Stl class.
:rtype: Stl
"""
stream.seek(0)
binary_header = "80sI"
binary_facet = "12fH"
# counter = 0
stream.write(struct.pack(binary_header, self.name.encode('utf8'),
len(self.triangles)))
# counter += 1
for triangle in self.triangles:
data = [
0., 0., 0.,
distance_multiplier * triangle.point1.x,
distance_multiplier * triangle.point1.y,
distance_multiplier * triangle.point1.z,
distance_multiplier * triangle.point2.x,
distance_multiplier * triangle.point2.y,
distance_multiplier * triangle.point2.z,
distance_multiplier * triangle.point3.x,
distance_multiplier * triangle.point3.y,
distance_multiplier * triangle.point3.z,
0]
stream.write(struct.pack(binary_facet, *data))
[docs]
def to_closed_shell(self):
"""
Convert the STL object to a closed triangle shell.
:return: A closed triangle shell representation of the STL object.
:rtype: shells.ClosedTriangleShell3D
"""
return shells.ClosedTriangleShell3D(self.triangles, name=self.name)
[docs]
def to_open_shell(self):
"""
Convert the STL object to an open triangle shell.
:return: An open triangle shell representation of the STL object.
:rtype: shells.OpenTriangleShell3D
"""
return shells.OpenTriangleShell3D(self.triangles, name=self.name)
[docs]
def to_volume_model(self):
"""
Convert the STL object to a volume model.
:return: A volume model representation of the STL object.
:rtype: d3dc.VolumeModel
"""
closed_shell = self.to_closed_shell()
return d3dc.VolumeModel([closed_shell], name=self.name)
# TODO: decide which algorithm to be used (no _BIS)
[docs]
@classmethod
def from_display_mesh(cls, mesh: d3d.display.Mesh3D):
"""
Create an STL object from a display mesh.
:param mesh: The display mesh to convert to an STL object.
:type mesh: d3d.display.DisplayMesh3D
:return: An instance of the Stl class.
:rtype: Stl
"""
triangles = []
for vertex1, vertex2, vertex3 in mesh.triangles_vertices:
point1 = d3d.Point3D(*vertex1)
point2 = d3d.Point3D(*vertex2)
point3 = d3d.Point3D(*vertex3)
triangles.append(d3df.Triangle3D(point1, point2, point3))
return cls(triangles)
[docs]
def get_normals(self):
"""
Gets normals.
points_normals : dictionary
returns a diction
"""
points_normals = {}
normals = []
for triangle in self.triangles:
normal = triangle.normal()
for point in triangle.points:
try:
points_normals[point].append(normal)
except KeyError:
points_normals[point] = [normal]
for key, value in points_normals.items():
point_normal = d3d.O3D
for point in value:
point_normal += point
points_normals[key] = point_normal
try:
point_normal = point_normal.unit_vector()
except ZeroDivisionError:
point_normal = value[0]
points_normals[key] = point_normal
point_normal = point_normal.unit_vector()
normals.append(point_normal)
self.normals = normals
return points_normals
[docs]
def clean_flat_triangles(self, threshold: float = 1e-12) -> 'Stl':
"""
Clean the STL object by removing flat triangles with an area below a threshold.
:return: A new instance of the Stl class with the flat triangles removed.
:rtype: Stl
"""
invalid_triangles = []
for index_t, triangles in enumerate(self.triangles):
if triangles.area() < threshold:
invalid_triangles.append(index_t)
triangles = self.triangles[:]
for invalid_triangle_index in invalid_triangles[::-1]:
triangles.pop(invalid_triangle_index)
return Stl(triangles)