// Author: Kirill Gavrilov // Copyright (c) 2015-2019 OPEN CASCADE SAS // // This file is part of Open CASCADE Technology software library. // // This library is free software; you can redistribute it and/or modify it under // the terms of the GNU Lesser General Public License version 2.1 as published // by the Free Software Foundation, with special exception defined in the file // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT // distribution for complete text of the license and disclaimer of any warranty. // // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. #ifndef _RWObj_Reader_HeaderFile #define _RWObj_Reader_HeaderFile #include "Message.hxx" #include "Message_Messenger.hxx" #include "Message_ProgressRange.hxx" #include "NCollection_Array1.hxx" #include "NCollection_DataMap.hxx" #include "NCollection_IndexedMap.hxx" #include "NCollection_DynamicArray.hxx" #include "NCollection_Shared.hxx" #include "OSD_OpenFile.hxx" #include "RWMesh_CoordinateSystemConverter.hxx" #include "RWObj_Material.hxx" #include "RWObj_SubMesh.hxx" #include "RWObj_SubMeshReason.hxx" #include "RWObj_Tools.hxx" #include "Standard_HashUtils.hxx" #include "NCollection_LinearVector.hxx" //! An abstract class implementing procedure to read OBJ file. //! //! This class is not bound to particular data structure //! and can be used to read the file directly into arbitrary data model. //! To use it, create descendant class and implement interface methods. //! //! Call method Read() to read the file. class RWObj_Reader : public Standard_Transient { DEFINE_STANDARD_RTTIEXT(RWObj_Reader, Standard_Transient) public: //! Empty constructor. Standard_EXPORT RWObj_Reader(); //! Open stream and pass it to Read method //! Returns true if success, false on error. bool Read(const TCollection_AsciiString& theFile, const Message_ProgressRange& theProgress) { std::ifstream aStream; OSD_OpenStream(aStream, theFile, std::ios_base::in | std::ios_base::binary); return Read(aStream, theFile, theProgress); } //! Reads data from OBJ file. //! Unicode paths can be given in UTF-8 encoding. //! Returns true if success, false on error or user break. bool Read(std::istream& theStream, const TCollection_AsciiString& theFile, const Message_ProgressRange& theProgress) { return read(theStream, theFile, theProgress, false); } //! Open stream and pass it to Probe method. //! @param theFile path to the file //! @param theProgress progress indicator //! @return TRUE if success, FALSE on error or user break. //! @sa FileComments(), ExternalFiles(), NbProbeNodes(), NbProbeElems(). bool Probe(const TCollection_AsciiString& theFile, const Message_ProgressRange& theProgress) { std::ifstream aStream; OSD_OpenStream(aStream, theFile, std::ios_base::in | std::ios_base::binary); return Probe(aStream, theFile, theProgress); } //! Probe data from OBJ file (comments, external references) without actually reading mesh data. //! Although mesh data will not be collected, the full file content will be parsed, due to OBJ //! format limitations. //! @param theStream input stream //! @param theFile path to the file //! @param theProgress progress indicator //! @return TRUE if success, FALSE on error or user break. //! @sa FileComments(), ExternalFiles(), NbProbeNodes(), NbProbeElems(). bool Probe(std::istream& theStream, const TCollection_AsciiString& theFile, const Message_ProgressRange& theProgress) { return read(theStream, theFile, theProgress, true); } //! Returns file comments (lines starting with # at the beginning of file). const TCollection_AsciiString& FileComments() const { return myFileComments; } //! Return the list of external file references. const NCollection_IndexedMap& ExternalFiles() const { return myExternalFiles; } //! Number of probed nodes. int NbProbeNodes() const { return myNbProbeNodes; } //!< number of probed polygon elements (of unknown size). int NbProbeElems() const { return myNbProbeElems; } //! Returns memory limit in bytes; -1 (no limit) by default. size_t MemoryLimit() const { return myMemLimitBytes; } //! Specify memory limit in bytes, so that import will be aborted //! by specified limit before memory allocation error occurs. void SetMemoryLimit(size_t theMemLimit) { myMemLimitBytes = theMemLimit; } //! Return transformation from one coordinate system to another; no transformation by default. const RWMesh_CoordinateSystemConverter& Transformation() const { return myCSTrsf; } //! Setup transformation from one coordinate system to another. //! OBJ file might be exported following various coordinate system conventions, //! so that it might be useful automatically transform data during file reading. void SetTransformation(const RWMesh_CoordinateSystemConverter& theCSConverter) { myCSTrsf = theCSConverter; } //! Return single precision flag for reading vertex data (coordinates); FALSE by default. bool IsSinglePrecision() const { return myObjVerts.IsSinglePrecision(); } //! Setup single/double precision flag for reading vertex data (coordinates). void SetSinglePrecision(bool theIsSinglePrecision) { myObjVerts.SetSinglePrecision(theIsSinglePrecision); } protected: //! Reads data from OBJ file. //! Unicode paths can be given in UTF-8 encoding. //! Returns true if success, false on error or user break. Standard_EXPORT bool read(std::istream& theStream, const TCollection_AsciiString& theFile, const Message_ProgressRange& theProgress, const bool theToProbe); //! @name interface methods which should be implemented by sub-class protected: //! Add new sub-mesh. //! Basically, this method will be called multiple times for the same group with different reason, //! so that implementation should decide if previously allocated sub-mesh should be used or new //! one to be allocated. Sub-mesh command can be skipped if previous sub-mesh is empty, or if the //! reason is out of interest for particular reader (e.g. if materials are ignored, reader may //! ignore RWObj_SubMeshReason_NewMaterial reason). //! @param theMesh mesh definition //! @param theReason reason to create new sub-mesh //! @return TRUE if new sub-mesh should be started since this point virtual bool addMesh(const RWObj_SubMesh& theMesh, const RWObj_SubMeshReason theReason) = 0; //! Retrieve sub-mesh node position, added by addNode(). virtual gp_Pnt getNode(int theIndex) const = 0; //! Callback function to be implemented in descendant. //! Should create new node with specified coordinates in the target model, and return its ID as //! integer. virtual int addNode(const gp_Pnt& thePnt) = 0; //! Callback function to be implemented in descendant. //! Should set normal coordinates for specified node. //! @param theIndex node ID as returned by addNode() //! @param theNorm normal vector virtual void setNodeNormal(const int theIndex, const NCollection_Vec3& theNorm) = 0; //! Callback function to be implemented in descendant. //! Should set texture coordinates for specified node. //! @param theIndex node ID as returned by addNode() //! @param theUV UV texture coordinates virtual void setNodeUV(const int theIndex, const NCollection_Vec2& theUV) = 0; //! Callback function to be implemented in descendant. //! Should create new element (triangle or quad if 4th index is != -1) built on specified nodes in //! the target model. virtual void addElement(int theN1, int theN2, int theN3, int theN4) = 0; //! @name implementation details private: //! Handle "v X Y Z". void pushVertex(const char* theXYZ) { char* aNext = nullptr; gp_Pnt anXYZ; RWObj_Tools::ReadVec3(theXYZ, aNext, anXYZ.ChangeCoord()); myCSTrsf.TransformPosition(anXYZ.ChangeCoord()); myMemEstim += myObjVerts.IsSinglePrecision() ? sizeof(NCollection_Vec3) : sizeof(gp_Pnt); myObjVerts.Append(anXYZ); } //! Handle "vn NX NY NZ". void pushNormal(const char* theXYZ) { char* aNext = nullptr; NCollection_Vec3 aNorm; RWObj_Tools::ReadVec3(theXYZ, aNext, aNorm); myCSTrsf.TransformNormal(aNorm); myMemEstim += sizeof(NCollection_Vec3); myObjNorms.Append(aNorm); } //! Handle "vt U V". void pushTexel(const char* theUV) { char* aNext = nullptr; NCollection_Vec2 anUV; anUV.x() = (float)Strtod(theUV, &aNext); theUV = aNext; anUV.y() = (float)Strtod(theUV, &aNext); myMemEstim += sizeof(NCollection_Vec2); myObjVertsUV.Append(anUV); } //! Handle "f indices". void pushIndices(const char* thePos); //! Compute the center of planar polygon. //! @param theIndices polygon indices //! @return center of polygon gp_XYZ polygonCenter(const NCollection_Array1& theIndices); //! Compute the normal to planar polygon. //! The logic is similar to ShapeAnalysis_Curve::IsPlanar(). //! @param theIndices polygon indices //! @return polygon normal gp_XYZ polygonNormal(const NCollection_Array1& theIndices); //! Create triangle fan from specified polygon. //! @param theIndices polygon nodes //! @return number of added triangles int triangulatePolygonFan(const NCollection_Array1& theIndices); //! Triangulate specified polygon. //! @param theIndices polygon nodes //! @return number of added triangles int triangulatePolygon(const NCollection_Array1& theIndices); //! Handle "o ObjectName". void pushObject(const char* theObjectName); //! Handle "g GroupName". void pushGroup(const char* theGroupName); //! Handle "s SmoothGroupIndex". void pushSmoothGroup(const char* theSmoothGroupIndex); //! Handle "usemtl MaterialName". void pushMaterial(const char* theMaterialName); //! Handle "mtllib FileName". void readMaterialLib(const char* theFileName); //! Check memory limits. //! @return FALSE on out of memory bool checkMemory(); protected: //! Hasher for 3 ordered integers. struct ObjVec3iHasher { std::size_t operator()(const NCollection_Vec3& theKey) const noexcept { return opencascade::hashBytes(&theKey[0], 3 * sizeof(int)); } bool operator()(const NCollection_Vec3& theKey1, const NCollection_Vec3& theKey2) const noexcept { return theKey1[0] == theKey2[0] && theKey1[1] == theKey2[1] && theKey1[2] == theKey2[2]; } }; //! Auxiliary structure holding vertex data either with single or double floating point precision. class VectorOfVertices { public: //! Empty constructor. VectorOfVertices() : myIsSinglePrecision(false) { } //! Return single precision flag; FALSE by default. bool IsSinglePrecision() const { return myIsSinglePrecision; } //! Setup single/double precision flag. void SetSinglePrecision(bool theIsSinglePrecision) { myIsSinglePrecision = theIsSinglePrecision; myPntVec.Nullify(); myVec3Vec.Nullify(); } //! Reset and (re)allocate buffer. void Reset() { if (myIsSinglePrecision) { myVec3Vec = new NCollection_Shared>>(); } else { myPntVec = new NCollection_Shared>(); } } //! Return vector lower index. int Lower() const { return 0; } //! Return vector upper index. int Upper() const { return myIsSinglePrecision ? myVec3Vec->Upper() : myPntVec->Upper(); } //! Return point with the given index. gp_Pnt Value(int theIndex) const { if (myIsSinglePrecision) { const NCollection_Vec3& aPnt = myVec3Vec->Value(theIndex); return gp_Pnt(aPnt.x(), aPnt.y(), aPnt.z()); } else { return myPntVec->Value(theIndex); } } //! Append new point. void Append(const gp_Pnt& thePnt) { if (myIsSinglePrecision) { myVec3Vec->Append( NCollection_Vec3((float)thePnt.X(), (float)thePnt.Y(), (float)thePnt.Z())); } else { myPntVec->Append(thePnt); } } private: Handle(NCollection_Shared>) myPntVec; Handle(NCollection_Shared>>) myVec3Vec; bool myIsSinglePrecision; }; protected: NCollection_IndexedMap myExternalFiles; //!< list of external file references TCollection_AsciiString myFileComments; //!< file header comments TCollection_AsciiString myFolder; //!< folder containing the OBJ file RWMesh_CoordinateSystemConverter myCSTrsf; //!< coordinate system flipper size_t myMemLimitBytes; //!< memory limit in bytes size_t myMemEstim; //!< estimated memory occupation in bytes // clang-format off int myNbLines; //!< number of parsed lines (e.g. current line) int myNbProbeNodes; //!< number of probed nodes int myNbProbeElems; //!< number of probed elements int myNbElemsBig; //!< number of big elements (polygons with 5+ nodes) bool myToAbort; //!< flag indicating abort state (e.g. syntax error) // clang-format on // Each node in the Element specifies independent indices of Vertex position, Texture coordinates // and Normal. This scheme does not match natural definition of Primitive Array where each unique // set of nodal properties defines Vertex (thus node at the same location but with different // normal should be duplicated). The following code converts OBJ definition of nodal properties to // Primitive Array definition. VectorOfVertices myObjVerts; //!< temporary vector of vertices NCollection_DynamicArray> myObjVertsUV; //!< temporary vector of UV parameters NCollection_DynamicArray> myObjNorms; //!< temporary vector of normals NCollection_DataMap, int, ObjVec3iHasher> myPackedIndices; NCollection_DataMap myMaterials; //!< map of known materials RWObj_SubMesh myActiveSubMesh; //!< active sub-mesh definition NCollection_LinearVector myCurrElem; //!< indices for the current element }; #endif // _RWObj_Reader_HeaderFile