# -*- coding: utf-8 -*-
#
# This file is subject to the terms and conditions defined in
# file 'LICENSE.txt', which is part of this source code package.
#
from typing import Optional
import numpy as np
from Muscat.Helpers.Logger import Info, Error
from Muscat.Containers.Mesh import Mesh
from Muscat.IO.IOFactory import RegisterReaderClass
import Muscat.Containers.ElementsDescription as ED
from Muscat.IO.ReaderBase import ReaderBase
from Muscat.Types import MuscatIndex, MuscatFloat
from Muscat.Containers.MeshModificationTools import CleanDoubleNodes
[docs]def ReadStl(fileName: str = "", string: str = "") -> Mesh:
"""Read Stl file into a Mesh
Parameters
----------
fileName : str, optional
The name of the file to read, by default None
string : str, optional
the string to read in the case of reading from memory, by default None
Returns
-------
Mesh
the stl surface
"""
obj = StlReader()
obj.SetFileName(fileName)
obj.SetStringToRead(string)
res = obj.Read()
return res
[docs]class StlReader(ReaderBase):
"""Stl read class, works for ASCII and binary format
"""
def __init__(self, fileName: str = '') -> None:
super().__init__(fileName=fileName)
self.runCleanDoubleNodes = True
[docs] def Read(self, fileName: Optional[str] = None, string: Optional[str] = None) -> Mesh:
""" Read a file or a string as a stl surface, ASCII and binary format are
supported.
Parameters
----------
fileName : str, optional
The name of the file to read, by default None
string : str, optional
the string to read in the case of reading from memory, by default None
out : Mesh, optional
output unstructured mesh object containing reading result, by default None
Returns
-------
Mesh
the read stl surface
"""
if fileName is not None:
self.SetFileName(fileName)
if string is not None:
self.SetStringToRead(string)
# check if binary
self.readFormat = "rb"
self.StartReading()
header = ""
# read the first non space characters to detect if binary or not
while len(header) < 5:
data = self.filePointer.read(1)
if data[0] < 128:
if chr(data[0]) == " ":
continue
header += chr(data[0])
if header == "solid":
Info("Ascii File")
res = self.ReadStlAscii()
else:
Info("Binary File")
res = self.ReadStlBinary()
return res
[docs] def ReadStlBinary(self) -> Mesh:
"""Read an binary stl file
Returns
-------
Mesh
the read stl surface
note: from https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL
"""
self.readFormat = "rb"
self.StartReading()
import Muscat.Containers.Mesh as UM
header = self.ReadData(80, np.int8)
hasMagicsColor = False
try:
header6 = ''.join([(chr(item) if item < 128 else " ") for item in header[0:6]])
if header6 == "COLOR=": # pragma: no cover
hasMagicsColor = True
header = ''.join([(chr(item) if item < 128 else " ") for item in header])
Info("HEADER : '" + header + "'")
except: # pragma: no cover
pass
nbTriangles = self.ReadInt32()
Info("reading : " + str(nbTriangles) + " triangles")
resUM = UM.Mesh()
# resUM.nodes = np.empty((nbTriangles*3,3), dtype=float)
dt = np.dtype([('normal', (np.float32, 3)),
('points', (np.float32, 9)),
('att', (np.uint16)),
])
data = self.ReadData(nbTriangles, dt)
normals = np.array(data["normal"])
resUM.nodes = np.array(data["points"], dtype=MuscatFloat)
resUM.nodes.shape = (nbTriangles*3, 3)
elements = resUM.GetElementsOfType(ED.Triangle_3)
elements.connectivity = np.array(range(resUM.GetNumberOfNodes()), dtype=MuscatIndex)
elements.connectivity.shape = (nbTriangles, 3)
elements.cpt = nbTriangles
resUM.elemFields["normals"] = normals
if hasMagicsColor: # pragma: no cover
Color = np.array(data["att"])
r = Color & 31
g = (Color >> 5) & 31
b = (Color >> 10) & 31
active = (Color >> 15)
resUM.elemFields["colors"] = np.vstack((r, g, b)).T
resUM.elemFields["activecolors"] = active.astype(dtype=np.int8)
self.EndReading()
resUM.GenerateManufacturedOriginalIDs()
resUM.PrepareForOutput()
if self.runCleanDoubleNodes:
boundingMin, boundingMax = resUM.ComputeBoundingBox()
tol = np.linalg.norm(boundingMax - boundingMin)*1e-15
CleanDoubleNodes(resUM, tol=tol)
return resUM
[docs] def ReadStlAscii(self) -> Mesh:
""" Read an ASCII stl file
Returns
-------
Mesh
the read stl surface
"""
self.readFormat = "r"
self.StartReading()
import Muscat.Containers.Mesh as UM
resUM = UM.Mesh()
name = self.ReadCleanLine().split()[1]
p = []
normals = np.empty((0, 3), dtype=float)
nodesBuffer = []
while True:
line = self.ReadCleanLine()
if not line:
break
l = line.strip('\n').lstrip().rstrip()
if l.find("facet") > -1:
if l.find("normal") > -1:
normals = np.concatenate((normals, np.fromstring(
l.split("normal")[1], sep=" ")[np.newaxis]), axis=0)
continue
if l.find("outer loop") > -1:
for i in range(3):
line = self.ReadCleanLine()
l = line.strip('\n').lstrip().rstrip()
if l.find("vertex") > -1:
p.append(np.fromstring(l.split("vertex")[1], sep=" "))
if len(p) == 3:
nodesBuffer.extend(p)
# resUM.nodes = np.vstack((resUM.nodes,p[0][np.newaxis,:],p[1][np.newaxis,:],p[2][np.newaxis,:]))
p = []
else: # pragma: no cover
Error("error: outer loop with less than 3 vertex")
raise
self.EndReading()
resUM.nodes = np.array(nodesBuffer, dtype=MuscatFloat)
elements = resUM.GetElementsOfType(ED.Triangle_3)
elements.connectivity = np.array(range(resUM.GetNumberOfNodes()), dtype=MuscatIndex)
elements.connectivity.shape = (resUM.GetNumberOfNodes()//3, 3)
elements.cpt = elements.connectivity.shape[0]
resUM.elemFields["normals"] = normals
resUM.GenerateManufacturedOriginalIDs()
resUM.PrepareForOutput()
if self.runCleanDoubleNodes:
boundingMin, boundingMax = resUM.ComputeBoundingBox()
tol = np.linalg.norm(boundingMax - boundingMin)*1e-15
CleanDoubleNodes(resUM, tol=tol)
return resUM
RegisterReaderClass(".stl", StlReader)
[docs]def CheckIntegrity():
data = """ solid cube_corner
facet normal 0.0 -1.0 0.0
outer loop
vertex 0.0 0.0 0.0
vertex 1.0 0.0 0.0
vertex 0.0 0.0 1.0
endloop
endfacet
facet normal 0.0 0.0 -1.0
outer loop
vertex 0.0 0.0 0.0
vertex 0.0 1.0 0.0
vertex 1.0 0.0 0.0
endloop
endfacet
facet normal -1.0 0.0 0.0
outer loop
vertex 0.0 0.0 0.0
vertex 0.0 0.0 1.0
vertex 0.0 1.0 0.0
endloop
endfacet
facet normal 0.577 0.577 0.577
outer loop
vertex 1.0 0.0 0.0
vertex 0.0 1.0 0.0
vertex 0.0 0.0 1.0
endloop
endfacet
endsolid
"""
obj = StlReader()
obj.Read(string=data)
from Muscat.Helpers.IO.FileTools import WriteTempFile
obj.Read(fileName=WriteTempFile("testMiniStl.stl", data))
res = ReadStl(string=data)
print(res)
np.testing.assert_equal(res.GetNumberOfNodes(), 4)
np.testing.assert_equal(res.GetNumberOfElements(), 4)
from Muscat.Helpers.IO.TemporaryDirectory import TemporaryDirectory
tempdir = TemporaryDirectory.GetTempPath()
f = open(tempdir+"test_input_stl_data.stl", "w")
f.write(data)
f.close()
res = ReadStl(fileName=tempdir+"test_input_stl_data.stl")
from Muscat.TestData import GetTestDataPath
print("Binary reading")
res1 = ReadStl(fileName=GetTestDataPath()+"coneBinary.stl")
print(res1)
print("Ascii reading")
res2 = ReadStl(fileName=GetTestDataPath()+"coneAscii.stl")
print(res2)
np.testing.assert_equal(res1.GetNumberOfNodes(), res2.GetNumberOfNodes())
# 1e-6 because the ascii file has only 6 decimals
np.testing.assert_allclose(res1.nodes, res2.nodes, rtol=1e-6)
np.testing.assert_equal(res1.GetNumberOfElements(), res2.GetNumberOfElements())
conn1 = res1.GetElementsOfType(ED.Triangle_3).connectivity
conn2 = res2.GetElementsOfType(ED.Triangle_3).connectivity
np.testing.assert_equal(conn1, conn2)
return 'ok'
if __name__ == '__main__':
print(CheckIntegrity()) # pragma: no cover