Source code for Muscat.IO.StlReader

# -*- 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