From 4151c94d20bae84a9798a37003dcac8eda732f3a Mon Sep 17 00:00:00 2001 From: kgv Date: Sun, 5 May 2019 20:31:35 +0300 Subject: [PATCH] 0029296: Data Exchange - implement import of mesh data from files in OBJ format RWObj_Reader and RWObj_CafReader - added new classes reading triangulation from OBJ file. --- adm/UDLIST | 1 + src/RWObj/FILES | 14 + src/RWObj/RWObj.cxx | 32 + src/RWObj/RWObj.hxx | 35 + src/RWObj/RWObj_CafReader.cxx | 104 +++ src/RWObj/RWObj_CafReader.hxx | 63 ++ src/RWObj/RWObj_Material.hxx | 43 ++ src/RWObj/RWObj_MtlReader.cxx | 351 ++++++++++ src/RWObj/RWObj_MtlReader.hxx | 58 ++ src/RWObj/RWObj_Reader.cxx | 829 ++++++++++++++++++++++++ src/RWObj/RWObj_Reader.hxx | 364 +++++++++++ src/RWObj/RWObj_SubMesh.hxx | 30 + src/RWObj/RWObj_SubMeshReason.hxx | 27 + src/RWObj/RWObj_Tools.hxx | 81 +++ src/RWObj/RWObj_TriangulationReader.cxx | 223 +++++++ src/RWObj/RWObj_TriangulationReader.hxx | 124 ++++ src/TKRWMesh/PACKAGES | 1 + src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx | 225 +++++++ tests/de_mesh/grids.list | 1 + tests/de_mesh/obj_read/begin | 2 + tests/de_mesh/obj_read/end | 6 + tests/de_mesh/obj_read/mustang | 9 + tests/de_mesh/obj_read/prism | 73 +++ tests/de_mesh/obj_read/ship_boat | 9 + 24 files changed, 2705 insertions(+) create mode 100644 src/RWObj/FILES create mode 100644 src/RWObj/RWObj.cxx create mode 100644 src/RWObj/RWObj.hxx create mode 100644 src/RWObj/RWObj_CafReader.cxx create mode 100644 src/RWObj/RWObj_CafReader.hxx create mode 100644 src/RWObj/RWObj_Material.hxx create mode 100644 src/RWObj/RWObj_MtlReader.cxx create mode 100644 src/RWObj/RWObj_MtlReader.hxx create mode 100644 src/RWObj/RWObj_Reader.cxx create mode 100644 src/RWObj/RWObj_Reader.hxx create mode 100644 src/RWObj/RWObj_SubMesh.hxx create mode 100644 src/RWObj/RWObj_SubMeshReason.hxx create mode 100644 src/RWObj/RWObj_Tools.hxx create mode 100644 src/RWObj/RWObj_TriangulationReader.cxx create mode 100644 src/RWObj/RWObj_TriangulationReader.hxx create mode 100644 tests/de_mesh/obj_read/begin create mode 100644 tests/de_mesh/obj_read/end create mode 100644 tests/de_mesh/obj_read/mustang create mode 100644 tests/de_mesh/obj_read/prism create mode 100644 tests/de_mesh/obj_read/ship_boat diff --git a/adm/UDLIST b/adm/UDLIST index 7f39f5eb0b..634ba2dfdb 100644 --- a/adm/UDLIST +++ b/adm/UDLIST @@ -442,3 +442,4 @@ n XCAFNoteObjects t TKRWMesh n RWGltf n RWMesh +n RWObj diff --git a/src/RWObj/FILES b/src/RWObj/FILES new file mode 100644 index 0000000000..f6a988e6fb --- /dev/null +++ b/src/RWObj/FILES @@ -0,0 +1,14 @@ +RWObj.cxx +RWObj.hxx +RWObj_CafReader.cxx +RWObj_CafReader.hxx +RWObj_Material.hxx +RWObj_MtlReader.cxx +RWObj_MtlReader.hxx +RWObj_Reader.cxx +RWObj_Reader.hxx +RWObj_SubMesh.hxx +RWObj_SubMeshReason.hxx +RWObj_Tools.hxx +RWObj_TriangulationReader.cxx +RWObj_TriangulationReader.hxx diff --git a/src/RWObj/RWObj.cxx b/src/RWObj/RWObj.cxx new file mode 100644 index 0000000000..60731abca6 --- /dev/null +++ b/src/RWObj/RWObj.cxx @@ -0,0 +1,32 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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. + +#include + +#include + +//============================================================================= +//function : Read +//purpose : +//============================================================================= +Handle(Poly_Triangulation) RWObj::ReadFile (const Standard_CString theFile, + const Handle(Message_ProgressIndicator)& theProgress) +{ + RWObj_TriangulationReader aReader; + aReader.SetCreateShapes (Standard_False); + aReader.Read (theFile, theProgress); + // note that returned bool value is ignored intentionally -- even if something went wrong, + // but some data have been read, we at least will return these data + return aReader.GetTriangulation(); +} diff --git a/src/RWObj/RWObj.hxx b/src/RWObj/RWObj.hxx new file mode 100644 index 0000000000..1d5035f6a6 --- /dev/null +++ b/src/RWObj/RWObj.hxx @@ -0,0 +1,35 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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_HeaderFile +#define _RWObj_HeaderFile + +#include +#include +#include +#include + +//! This class provides methods to read and write triangulation from / to the OBJ files. +class RWObj +{ +public: + + //! Read specified OBJ file and returns its content as triangulation. + //! In case of error, returns Null handle. + Standard_EXPORT static Handle(Poly_Triangulation) ReadFile (const Standard_CString theFile, + const Handle(Message_ProgressIndicator)& aProgInd = NULL); + +}; + +#endif // _RWObj_HeaderFile diff --git a/src/RWObj/RWObj_CafReader.cxx b/src/RWObj/RWObj_CafReader.cxx new file mode 100644 index 0000000000..2ff06cec99 --- /dev/null +++ b/src/RWObj/RWObj_CafReader.cxx @@ -0,0 +1,104 @@ +// Author: Kirill Gavrilov +// Copyright (c) 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. + +#include + +#include + +IMPLEMENT_STANDARD_RTTIEXT(RWObj_CafReader, RWMesh_CafReader) + +//================================================================ +// Function : Constructor +// Purpose : +//================================================================ +RWObj_CafReader::RWObj_CafReader() +: myIsSinglePrecision (Standard_False) +{ + //myCoordSysConverter.SetInputLengthUnit (-1.0); // length units are undefined within OBJ file + // OBJ format does not define coordinate system (apart from mentioning that it is right-handed), + // however most files are stored Y-up + myCoordSysConverter.SetInputCoordinateSystem (RWMesh_CoordinateSystem_glTF); +} + +//================================================================ +// Function : BindNamedShape +// Purpose : +//================================================================ +void RWObj_CafReader::BindNamedShape (const TopoDS_Shape& theShape, + const TCollection_AsciiString& theName, + const RWObj_Material* theMaterial, + const Standard_Boolean theIsRootShape) +{ + if (theShape.IsNull()) + { + return; + } + + RWMesh_NodeAttributes aShapeAttribs; + aShapeAttribs.Name = theName; + if (theMaterial != NULL) + { + aShapeAttribs.Style.SetColorSurf (Quantity_ColorRGBA (theMaterial->DiffuseColor, 1.0f - theMaterial->Transparency)); + } + myAttribMap.Bind (theShape, aShapeAttribs); + + if (theIsRootShape) + { + myRootShapes.Append (theShape); + } +} + +//================================================================ +// Function : createReaderContext +// Purpose : +//================================================================ +Handle(RWObj_TriangulationReader) RWObj_CafReader::createReaderContext() +{ + Handle(RWObj_TriangulationReader) aReader = new RWObj_TriangulationReader(); + return aReader; +} + +//================================================================ +// Function : performMesh +// Purpose : +//================================================================ +Standard_Boolean RWObj_CafReader::performMesh (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress, + const Standard_Boolean theToProbe) +{ + Handle(RWObj_TriangulationReader) aCtx = createReaderContext(); + aCtx->SetSinglePrecision (myIsSinglePrecision); + aCtx->SetCreateShapes (Standard_True); + aCtx->SetShapeReceiver (this); + aCtx->SetTransformation (myCoordSysConverter); + aCtx->SetMemoryLimit (myMemoryLimitMiB == -1 ? Standard_Size(-1) : Standard_Size(myMemoryLimitMiB * 1024 * 1024)); + Standard_Boolean isDone = Standard_False; + if (theToProbe) + { + isDone = aCtx->Probe (theFile.ToCString(), theProgress); + } + else + { + isDone = aCtx->Read (theFile.ToCString(), theProgress); + } + if (!aCtx->FileComments().IsEmpty()) + { + myMetadata.Add ("Comments", aCtx->FileComments()); + } + for (NCollection_IndexedMap::Iterator aFileIter (aCtx->ExternalFiles()); aFileIter.More(); aFileIter.Next()) + { + myExternalFiles.Add (aFileIter.Value()); + } + return isDone; +} diff --git a/src/RWObj/RWObj_CafReader.hxx b/src/RWObj/RWObj_CafReader.hxx new file mode 100644 index 0000000000..a924a5f53b --- /dev/null +++ b/src/RWObj/RWObj_CafReader.hxx @@ -0,0 +1,63 @@ +// Copyright (c) 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_CafReader_HeaderFile +#define _RWObj_CafReader_HeaderFile + +#include +#include + +//! The OBJ mesh reader into XDE document. +class RWObj_CafReader : public RWMesh_CafReader, protected RWObj_IShapeReceiver +{ + DEFINE_STANDARD_RTTIEXT(RWObj_CafReader, RWMesh_CafReader) +public: + + //! Empty constructor. + Standard_EXPORT RWObj_CafReader(); + + //! Return single precision flag for reading vertex data (coordinates); FALSE by default. + Standard_Boolean IsSinglePrecision() const { return myIsSinglePrecision; } + + //! Setup single/double precision flag for reading vertex data (coordinates). + void SetSinglePrecision (Standard_Boolean theIsSinglePrecision) { myIsSinglePrecision = theIsSinglePrecision; } + +protected: + + //! Read the mesh from specified file. + Standard_EXPORT virtual Standard_Boolean performMesh (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress, + const Standard_Boolean theToProbe) Standard_OVERRIDE; + +protected: + + //! Create reader context. + //! Can be overridden by sub-class to read triangulation into application-specific data structures instead of Poly_Triangulation. + virtual Handle(RWObj_TriangulationReader) createReaderContext(); + + //! @param theShape shape to register + //! @param theName shape name + //! @param theMaterial shape material + //! @param theIsRootShape indicates that this is a root object (free shape) + virtual void BindNamedShape (const TopoDS_Shape& theShape, + const TCollection_AsciiString& theName, + const RWObj_Material* theMaterial, + const Standard_Boolean theIsRootShape) Standard_OVERRIDE; + +protected: + + Standard_Boolean myIsSinglePrecision; //!< flag for reading vertex data with single or double floating point precision + +}; + +#endif // _RWObj_CafReader_HeaderFile diff --git a/src/RWObj/RWObj_Material.hxx b/src/RWObj/RWObj_Material.hxx new file mode 100644 index 0000000000..b47bea5b7c --- /dev/null +++ b/src/RWObj/RWObj_Material.hxx @@ -0,0 +1,43 @@ +// 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_Material_HeaderFile +#define _RWObj_Material_HeaderFile + +#include +#include + +//! Material definition for OBJ file format. +struct RWObj_Material +{ + TCollection_AsciiString Name; //!< material name (identifier) as defined in MTL file + TCollection_AsciiString DiffuseTexture; //!< path to the texture image file defining diffuse color + TCollection_AsciiString SpecularTexture; //!< path to the texture image file defining specular color + TCollection_AsciiString BumpTexture; //!< path to the texture image file defining normal map + Quantity_Color AmbientColor; + Quantity_Color DiffuseColor; + Quantity_Color SpecularColor; + Standard_ShortReal Shininess; + Standard_ShortReal Transparency; + + RWObj_Material() + : AmbientColor (0.1, 0.1, 0.1, Quantity_TOC_RGB), + DiffuseColor (0.8, 0.8, 0.8, Quantity_TOC_RGB), + SpecularColor(0.2, 0.2, 0.2, Quantity_TOC_RGB), + Shininess (1.0f), + Transparency (0.0f) {} + +}; + +#endif // _RWObj_Material_HeaderFile diff --git a/src/RWObj/RWObj_MtlReader.cxx b/src/RWObj/RWObj_MtlReader.cxx new file mode 100644 index 0000000000..549d83ce79 --- /dev/null +++ b/src/RWObj/RWObj_MtlReader.cxx @@ -0,0 +1,351 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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. + +#include + +#include + +#include +#include +#include +#include +#include + +namespace +{ + //! Try to find a new location of the file relative to specified folder from absolute path. + //! @param theAbsolutePath original absolute file path + //! @param theNewFoler the new folder to look for the file + //! @param theRelativePath result file path relative to theNewFoler + //! @return true if relative file has been found + static bool findRelativePath (const TCollection_AsciiString& theAbsolutePath, + const TCollection_AsciiString& theNewFoler, + TCollection_AsciiString& theRelativePath) + { + TCollection_AsciiString aNewFoler = (theNewFoler.EndsWith ("\\") || theNewFoler.EndsWith ("/")) + ? theNewFoler + : (theNewFoler + "/"); + + TCollection_AsciiString aRelPath; + TCollection_AsciiString aPath = theAbsolutePath; + for (;;) + { + TCollection_AsciiString aFolder, aFileName; + OSD_Path::FolderAndFileFromPath (aPath, aFolder, aFileName); + if (aFolder.IsEmpty() + || aFileName.IsEmpty()) + { + return false; + } + + if (aRelPath.IsEmpty()) + { + aRelPath = aFileName; + } + else + { + aRelPath = aFileName + "/" + aRelPath; + } + + if (OSD_File (aNewFoler + aRelPath).Exists()) + { + theRelativePath = aRelPath; + return true; + } + aPath = aFolder; + } + } +} + +// ======================================================================= +// function : RWObj_MtlReader +// purpose : +// ======================================================================= +RWObj_MtlReader::RWObj_MtlReader (NCollection_DataMap& theMaterials) +: myFile (NULL), + myMaterials (&theMaterials), + myNbLines (0) +{ + // +} + +// ======================================================================= +// function : ~RWObj_MtlReader +// purpose : +// ======================================================================= +RWObj_MtlReader::~RWObj_MtlReader() +{ + if (myFile != NULL) + { + ::fclose (myFile); + } +} + +// ======================================================================= +// function : Read +// purpose : +// ======================================================================= +bool RWObj_MtlReader::Read (const TCollection_AsciiString& theFolder, + const TCollection_AsciiString& theFile) +{ + myPath = theFolder + theFile; + myFile = OSD_OpenFile (myPath.ToCString(), "rb"); + if (myFile == NULL) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString ("OBJ material file '") + myPath + "' is not found!", Message_Warning); + return Standard_False; + } + + char aLine[256] = {}; + TCollection_AsciiString aMatName; + RWObj_Material aMat; + const Standard_Integer aNbMatOld = myMaterials->Extent(); + bool hasAspect = false; + for (; ::feof (myFile) == 0 && ::fgets (aLine, 255, myFile) != NULL; ) + { + ++myNbLines; + + const char* aPos = aLine; + + // skip spaces + for (; IsSpace(*aPos);) + { + ++aPos; + } + + if (*aPos == '#' + || *aPos == '\n' + || *aPos == '\0') + { + continue; + } + + if (::memcmp (aPos, "newmtl", 6) == 0) + { + aPos += 7; + if (!aMatName.IsEmpty()) + { + if (hasAspect) + { + aMat.Name = aMatName; + } + else + { + // reset incomplete material definition + aMat = RWObj_Material(); + } + myMaterials->Bind (aMatName, aMat); + hasAspect = false; + } + + aMatName = TCollection_AsciiString(aPos); + aMat = RWObj_Material(); + if (!RWObj_Tools::ReadName (aPos, aMatName)) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Empty OBJ material at line ") + myNbLines + " in file " + myPath, Message_Warning); + } + } + else if (::memcmp (aPos, "Ka", 2) == 0 + && IsSpace (aPos[2])) + { + aPos += 3; + char* aNext = NULL; + Graphic3d_Vec3 aColor; + RWObj_Tools::ReadVec3 (aPos, aNext, aColor); + aPos = aNext; + if (validateColor (aColor)) + { + aMat.AmbientColor = Quantity_Color (aColor.r(), aColor.g(), aColor.b(), Quantity_TOC_RGB); + hasAspect = true; + } + } + else if (::memcmp (aPos, "Kd", 2) == 0 + && IsSpace (aPos[2])) + { + aPos += 3; + char* aNext = NULL; + Graphic3d_Vec3 aColor; + RWObj_Tools::ReadVec3 (aPos, aNext, aColor); + aPos = aNext; + if (validateColor (aColor)) + { + aMat.DiffuseColor = Quantity_Color (aColor.r(), aColor.g(), aColor.b(), Quantity_TOC_RGB); + hasAspect = true; + } + } + else if (::memcmp (aPos, "Ks", 2) == 0 + && IsSpace (aPos[2])) + { + aPos += 3; + char* aNext = NULL; + Graphic3d_Vec3 aColor; + RWObj_Tools::ReadVec3 (aPos, aNext, aColor); + aPos = aNext; + if (validateColor (aColor)) + { + aMat.SpecularColor = Quantity_Color (aColor.r(), aColor.g(), aColor.b(), Quantity_TOC_RGB); + } + } + else if (::memcmp (aPos, "Ns", 2) == 0 + && IsSpace (aPos[2])) + { + aPos += 3; + char* aNext = NULL; + double aSpecular = Strtod (aPos, &aNext); + aPos = aNext; + if (aSpecular >= 0.0) + { + aMat.Shininess = (float )Min (aSpecular / 1000.0, 1.0); + } + } + else if (::memcmp (aPos, "Tr", 2) == 0 + && IsSpace (aPos[2])) + { + aPos += 3; + char* aNext = NULL; + double aTransp = Strtod (aPos, &aNext); + aPos = aNext; + if (validateScalar (aTransp) + && aTransp <= 0.99) + { + aMat.Transparency = (float )aTransp; + } + } + else if (*aPos == 'd' && IsSpace (aPos[1])) + { + // dissolve + aPos += 2; + char* aNext = NULL; + double anAlpha = Strtod (aPos, &aNext); + aPos = aNext; + if (validateScalar (anAlpha) + && anAlpha >= 0.01) + { + aMat.Transparency = float(1.0 - anAlpha); + } + } + else if (::memcmp (aPos, "map_Kd", 6) == 0 + && IsSpace (aPos[6])) + { + aPos += 7; + if (RWObj_Tools::ReadName (aPos, aMat.DiffuseTexture)) + { + processTexturePath (aMat.DiffuseTexture, theFolder); + } + } + else if (::memcmp (aPos, "map_Ks", 6) == 0 + && IsSpace (aPos[6])) + { + aPos += 7; + if (RWObj_Tools::ReadName (aPos, aMat.SpecularTexture)) + { + processTexturePath (aMat.SpecularTexture, theFolder); + } + } + else if (::memcmp (aPos, "map_Bump", 8) == 0 + && IsSpace (aPos[8])) + { + aPos += 9; + if (RWObj_Tools::ReadName (aPos, aMat.BumpTexture)) + { + processTexturePath (aMat.BumpTexture, theFolder); + } + } + /*else if (::memcmp (aPos, "illum", 5) == 0) + { + aPos += 6; + char* aNext = NULL; + const int aModel = strtol (aPos, &aNext, 10); + aPos = aNext; + if (aModel < 0 || aModel > 10) + { + // unknown model + } + }*/ + } + + if (!aMatName.IsEmpty()) + { + if (hasAspect) + { + aMat.Name = aMatName; + } + else + { + // reset incomplete material definition + aMat = RWObj_Material(); + } + myMaterials->Bind (aMatName, aMat); + } + + return myMaterials->Extent() != aNbMatOld; +} + +// ======================================================================= +// function : processTexturePath +// purpose : +// ======================================================================= +void RWObj_MtlReader::processTexturePath (TCollection_AsciiString& theTexturePath, + const TCollection_AsciiString& theFolder) +{ + if (OSD_Path::IsAbsolutePath (theTexturePath.ToCString())) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("OBJ file specifies absolute path to the texture image file which may be inaccessible on another device\n") + + theTexturePath, Message_Warning); + if (!OSD_File (theTexturePath).Exists()) + { + // workaround absolute filenames - try to find the same file at the OBJ file location + TCollection_AsciiString aRelativePath; + if (findRelativePath (theTexturePath, theFolder, aRelativePath)) + { + theTexturePath = theFolder + aRelativePath; + } + } + } + else + { + theTexturePath = theFolder + theTexturePath; + } +} + +// ======================================================================= +// function : validateScalar +// purpose : +// ======================================================================= +bool RWObj_MtlReader::validateScalar (const Standard_Real theValue) +{ + if (theValue < 0.0 + || theValue > 1.0) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Invalid scalar in OBJ material at line ") + myNbLines + " in file " + myPath, Message_Warning); + return false; + } + return true; +} + +// ======================================================================= +// function : validateColor +// purpose : +// ======================================================================= +bool RWObj_MtlReader::validateColor (const Graphic3d_Vec3& theVec) +{ + if (theVec.r() < 0.0f || theVec.r() > 1.0f + || theVec.g() < 0.0f || theVec.g() > 1.0f + || theVec.b() < 0.0f || theVec.b() > 1.0f) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Invalid color in OBJ material at line ") + myNbLines + " in file " + myPath, Message_Warning); + return false; + } + return true; +} diff --git a/src/RWObj/RWObj_MtlReader.hxx b/src/RWObj/RWObj_MtlReader.hxx new file mode 100644 index 0000000000..e2d1a07b8e --- /dev/null +++ b/src/RWObj/RWObj_MtlReader.hxx @@ -0,0 +1,58 @@ +// 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_MtlReader_HeaderFile +#define _RWObj_MtlReader_HeaderFile + +#include +#include +#include + +//! Reader of mtl files. +class RWObj_MtlReader +{ +public: + + //! Main constructor. + RWObj_MtlReader (NCollection_DataMap& theMaterials); + + //! Destructor. + ~RWObj_MtlReader(); + + //! Read the file. + bool Read (const TCollection_AsciiString& theFolder, + const TCollection_AsciiString& theFile); + +private: + + //! Validate scalar value + bool validateScalar (const Standard_Real theValue); + + //! Validate RGB color + bool validateColor (const Graphic3d_Vec3& theVec); + + //! Process texture path. + void processTexturePath (TCollection_AsciiString& theTexturePath, + const TCollection_AsciiString& theFolder); + +private: + + FILE* myFile; + TCollection_AsciiString myPath; + NCollection_DataMap* myMaterials; + int myNbLines; + +}; + +#endif // _RWObj_MtlReader_HeaderFile diff --git a/src/RWObj/RWObj_Reader.cxx b/src/RWObj/RWObj_Reader.cxx new file mode 100644 index 0000000000..0d9b74aac9 --- /dev/null +++ b/src/RWObj/RWObj_Reader.cxx @@ -0,0 +1,829 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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. + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if defined(_WIN32) + #define ftell64(a) _ftelli64(a) + #define fseek64(a,b,c) _fseeki64(a,b,c) +#else + #define ftell64(a) ftello(a) + #define fseek64(a,b,c) fseeko(a,b,c) +#endif + +IMPLEMENT_STANDARD_RTTIEXT(RWObj_Reader, Standard_Transient) + +namespace +{ + + //! Simple wrapper. + struct RWObj_ReaderFile + { + FILE* File; + NCollection_Array1 Line; + Standard_Integer LineBuffLen; + Standard_Integer MaxLineLen; + int64_t Position; + int64_t FileLen; + + //! Constructor opening the file. + RWObj_ReaderFile (const TCollection_AsciiString& theFile, + const Standard_Integer theMaxLineLen = 256) + : File (OSD_OpenFile (theFile.ToCString(), "rb")), + Line (0, theMaxLineLen - 1), + LineBuffLen (theMaxLineLen), + MaxLineLen (theMaxLineLen), + Position (0), + FileLen (0) + { + if (this->File != NULL) + { + // determine length of file + ::fseek64 (this->File, 0, SEEK_END); + FileLen = ::ftell64 (this->File); + ::fseek64 (this->File, 0, SEEK_SET); + } + } + + //! Destructor closing the file. + ~RWObj_ReaderFile() + { + if (File != NULL) + { + ::fclose (File); + } + } + + //! Read line, also considers multi-line syntax (when last line symbol is slash). + bool ReadLine() + { + int64_t aPosPrev = this->Position; + char* aLine = &Line.ChangeFirst(); + for (; ::feof (this->File) == 0 && ::fgets (aLine, MaxLineLen - 1, this->File) != NULL; ) + { + const int64_t aPosNew = ::ftell64 (this->File); + if (aLine[0] == '#') + { + Position = aPosNew; + return true; + } + + const Standard_Integer aNbRead = Standard_Integer(aPosNew - aPosPrev); + bool toReadMore = false; + for (int aTailIter = aNbRead - 1; aTailIter >= 0; --aTailIter) + { + if (aLine[aTailIter] != '\n' + && aLine[aTailIter] != '\r' + && aLine[aTailIter] != '\0') + { + if (aLine[aTailIter] == '\\') + { + // multi-line syntax + aLine[aTailIter] = ' '; + const ptrdiff_t aFullLen = aLine + aTailIter + 1 - &this->Line.First(); + if (LineBuffLen < aNbRead + MaxLineLen) + { + LineBuffLen += MaxLineLen; + this->Line.Resize (0, LineBuffLen - 1, true); + } + aLine = &this->Line.ChangeFirst() + aFullLen; + toReadMore = true; + break; + } + break; + } + } + + if (toReadMore) + { + aPosPrev = aPosNew; + continue; + } + + Position = aPosNew; + return true; + } + return false; + } + + }; + + //! Return TRUE if given polygon has clockwise node order. + static bool isClockwisePolygon (const Handle(BRepMesh_DataStructureOfDelaun)& theMesh, + const IMeshData::VectorOfInteger& theIndexes) + { + double aPtSum = 0; + const int aNbElemNodes = theIndexes.Size(); + for (int aNodeIter = theIndexes.Lower(); aNodeIter <= theIndexes.Upper(); ++aNodeIter) + { + int aNodeNext = theIndexes.Lower() + ((aNodeIter + 1) % aNbElemNodes); + const BRepMesh_Vertex& aVert1 = theMesh->GetNode (theIndexes.Value (aNodeIter)); + const BRepMesh_Vertex& aVert2 = theMesh->GetNode (theIndexes.Value (aNodeNext)); + aPtSum += (aVert2.Coord().X() - aVert1.Coord().X()) + * (aVert2.Coord().Y() + aVert1.Coord().Y()); + } + return aPtSum < 0.0; + } +} + +// ================================================================ +// Function : Read +// Purpose : +// ================================================================ +RWObj_Reader::RWObj_Reader() +: myMemLimitBytes (Standard_Size(-1)), + myMemEstim (0), + myNbLines (0), + myNbProbeNodes (0), + myNbProbeElems (0), + myNbElemsBig (0), + myToAbort (false) +{ + // +} + +// ================================================================ +// Function : read +// Purpose : +// ================================================================ +Standard_Boolean RWObj_Reader::read (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress, + const Standard_Boolean theToProbe) +{ + myMemEstim = 0; + myNbLines = 0; + myNbProbeNodes = 0; + myNbProbeElems = 0; + myNbElemsBig = 0; + myToAbort = false; + myObjVerts.Reset(); + myObjVertsUV.Clear(); + myObjNorms.Clear(); + myPackedIndices.Clear(); + myMaterials.Clear(); + myFileComments.Clear(); + myExternalFiles.Clear(); + myActiveSubMesh = RWObj_SubMesh(); + + // determine file location to load associated files + TCollection_AsciiString aFileName; + OSD_Path::FolderAndFileFromPath (theFile, myFolder, aFileName); + myCurrElem.resize (1024, -1); + + Standard_CLocaleSentry aLocaleSentry; + RWObj_ReaderFile aFile (theFile); + if (aFile.File == NULL) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString ("Error: file '") + theFile + "' is not found!", Message_Fail); + return Standard_False; + } + + // determine length of file + const int64_t aFileLen = aFile.FileLen; + if (aFileLen <= 0L) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString ("Error: file '") + theFile + "' is empty!", Message_Fail); + return Standard_False; + } + + const Standard_Integer aNbMiBTotal = Standard_Integer(aFileLen / (1024 * 1024)); + Standard_Integer aNbMiBPassed = 0; + Message_ProgressSentry aPSentry (theProgress, "Reading text OBJ file", 0, aNbMiBTotal, 1); + OSD_Timer aTimer; + aTimer.Start(); + + bool isStart = true; + for (; aFile.ReadLine(); ) + { + ++myNbLines; + const char* aLine = &aFile.Line.First(); + if (aTimer.ElapsedTime() > 1.0) + { + if (!aPSentry.More()) + { + return false; + } + + const Standard_Integer aNbMiBRead = Standard_Integer(aFile.Position / (1024 * 1024)); + for (; aNbMiBPassed < aNbMiBRead; ++aNbMiBPassed) { aPSentry.Next(); } + aTimer.Reset(); + aTimer.Start(); + } + + if (*aLine == '#') + { + if (isStart) + { + TCollection_AsciiString aComment (aLine + 1); + aComment.LeftAdjust(); + aComment.RightAdjust(); + if (!aComment.IsEmpty()) + { + if (!myFileComments.IsEmpty()) + { + myFileComments += "\n"; + } + myFileComments += aComment; + } + } + continue; + } + else if (*aLine == '\n' + || *aLine == '\0') + { + + continue; + } + isStart = false; + + if (theToProbe) + { + if (::memcmp (aLine, "mtllib", 6) == 0) + { + readMaterialLib (aLine + 7); + } + else if (aLine[0] == 'v' && RWObj_Tools::isSpaceChar (aLine[1])) + { + ++myNbProbeNodes; + } + else if (aLine[0] == 'f' && RWObj_Tools::isSpaceChar (aLine[1])) + { + ++myNbProbeElems; + } + continue; + } + + if (aLine[0] == 'v' && RWObj_Tools::isSpaceChar (aLine[1])) + { + ++myNbProbeNodes; + pushVertex (aLine + 2); + } + else if (aLine[0] == 'v' + && aLine[1] == 'n' + && RWObj_Tools::isSpaceChar (aLine[2])) + { + pushNormal (aLine + 3); + } + else if (aLine[0] == 'v' + && aLine[1] == 't' + && RWObj_Tools::isSpaceChar (aLine[2])) + { + pushTexel (aLine + 3); + } + else if (aLine[0] == 'f' && RWObj_Tools::isSpaceChar (aLine[1])) + { + ++myNbProbeElems; + pushIndices (aLine + 2); + } + else if (aLine[0] == 'g' && IsSpace (aLine[1])) + { + pushGroup (aLine + 2); + } + else if (aLine[0] == 's' && IsSpace (aLine[1])) + { + pushSmoothGroup (aLine + 2); + } + else if (aLine[0] == 'o' && IsSpace (aLine[1])) + { + pushObject (aLine + 2); + } + else if (::memcmp (aLine, "mtllib", 6) == 0) + { + readMaterialLib (aLine + 7); + } + else if (::memcmp (aLine, "usemtl", 6) == 0) + { + pushMaterial (aLine + 7); + } + + if (!checkMemory()) + { + addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewObject); + return false; + } + } + + // collect external references + for (NCollection_DataMap::Iterator aMatIter (myMaterials); aMatIter.More(); aMatIter.Next()) + { + const RWObj_Material& aMat = aMatIter.Value(); + if (!aMat.DiffuseTexture.IsEmpty()) + { + myExternalFiles.Add (aMat.DiffuseTexture); + } + if (!aMat.SpecularTexture.IsEmpty()) + { + myExternalFiles.Add (aMat.SpecularTexture); + } + if (!aMat.BumpTexture.IsEmpty()) + { + myExternalFiles.Add (aMat.BumpTexture); + } + } + + // flush the last group + if (!theToProbe) + { + addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewObject); + } + if (myNbElemsBig != 0) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: OBJ reader, ") + myNbElemsBig + + " polygon(s) have been split into triangles.", Message_Warning); + } + + for (; aNbMiBPassed < aNbMiBTotal; ++aNbMiBPassed) { aPSentry.Next(); } + return true; +} + +// ======================================================================= +// function : pushIndices +// purpose : +// ======================================================================= +void RWObj_Reader::pushIndices (const char* thePos) +{ + char* aNext = NULL; + + Standard_Integer aNbElemNodes = 0; + for (Standard_Integer aNode = 0;; ++aNode) + { + Graphic3d_Vec3i a3Indices (-1, -1, -1); + a3Indices[0] = strtol (thePos, &aNext, 10) - 1; + if (aNext == thePos) + { + break; + } + + // parse UV index + thePos = aNext; + if (*thePos == '/') + { + ++thePos; + a3Indices[1] = strtol (thePos, &aNext, 10) - 1; + thePos = aNext; + + // parse Normal index + if (*thePos == '/') + { + ++thePos; + a3Indices[2] = strtol (thePos, &aNext, 10) - 1; + thePos = aNext; + } + } + + // handle negative indices + if (a3Indices[0] < -1) + { + a3Indices[0] += myObjVerts.Upper() + 2; + } + if (a3Indices[1] < -1) + { + a3Indices[1] += myObjVertsUV.Upper() + 2; + } + if (a3Indices[2] < -1) + { + a3Indices[2] += myObjNorms.Upper() + 2; + } + + Standard_Integer anIndex = -1; + if (!myPackedIndices.Find (a3Indices, anIndex)) + { + if (a3Indices[0] >= 0) + { + myMemEstim += sizeof(Graphic3d_Vec3); + } + if (a3Indices[1] >= 0) + { + myMemEstim += sizeof(Graphic3d_Vec2); + } + if (a3Indices[2] >= 0) + { + myMemEstim += sizeof(Graphic3d_Vec3); + } + myMemEstim += sizeof(Graphic3d_Vec4i) + sizeof(Standard_Integer); // naive map + if (a3Indices[0] < myObjVerts.Lower() || a3Indices[0] > myObjVerts.Upper()) + { + myToAbort = true; + Message::DefaultMessenger()->Send (TCollection_AsciiString("Error: invalid OBJ syntax at line ") + myNbLines + + ": vertex index is out of range.", Message_Fail); + return; + } + + anIndex = addNode (myObjVerts.Value (a3Indices[0])); + myPackedIndices.Bind (a3Indices, anIndex); + if (a3Indices[1] >= 0) + { + if (myObjVertsUV.IsEmpty()) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: invalid OBJ syntax at line ") + myNbLines + + ": UV index is specified but no UV nodes are defined.", Message_Warning); + } + else if (a3Indices[1] < myObjVertsUV.Lower() || a3Indices[1] > myObjVertsUV.Upper()) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: invalid OBJ syntax at line ") + myNbLines + + ": UV index is out of range.", Message_Warning); + setNodeUV (anIndex,Graphic3d_Vec2 (0.0f, 0.0f)); + } + else + { + setNodeUV (anIndex, myObjVertsUV.Value (a3Indices[1])); + } + } + if (a3Indices[2] >= 0) + { + if (myObjNorms.IsEmpty()) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: invalid OBJ syntax at line ") + myNbLines + + ": Normal index is specified but no Normals nodes are defined.", Message_Warning); + } + else if (a3Indices[2] < myObjNorms.Lower() || a3Indices[2] > myObjNorms.Upper()) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: invalid OBJ syntax at line ") + myNbLines + + ": Normal index is out of range.", Message_Warning); + setNodeNormal (anIndex, Graphic3d_Vec3 (0.0f, 0.0f, 1.0f)); + } + else + { + setNodeNormal (anIndex, myObjNorms.Value (a3Indices[2])); + } + } + } + + if (myCurrElem.size() < size_t(aNode)) + { + myCurrElem.resize (aNode * 2, -1); + } + myCurrElem[aNode] = anIndex; + aNbElemNodes = aNode + 1; + + if (*thePos == '\n' + || *thePos == '\0') + { + break; + } + + if (*thePos != ' ') + { + ++thePos; + } + } + + if (myCurrElem[0] < 0 + || myCurrElem[1] < 0 + || myCurrElem[2] < 0 + || aNbElemNodes < 3) + { + return; + } + + if (aNbElemNodes == 3) + { + myMemEstim += sizeof(Graphic3d_Vec4i); + addElement (myCurrElem[0], myCurrElem[1], myCurrElem[2], -1); + } + else if (aNbElemNodes == 4) + { + myMemEstim += sizeof(Graphic3d_Vec4i); + addElement (myCurrElem[0], myCurrElem[1], myCurrElem[2], myCurrElem[3]); + } + else + { + const NCollection_Array1 aCurrElemArray1 (myCurrElem[0], 1, aNbElemNodes); + const Standard_Integer aNbAdded = triangulatePolygon (aCurrElemArray1); + if (aNbAdded < 1) + { + return; + } + ++myNbElemsBig; + myMemEstim += sizeof(Graphic3d_Vec4i) * aNbAdded; + } +} + +//================================================================ +// Function : triangulatePolygonFan +// Purpose : +//================================================================ +Standard_Integer RWObj_Reader::triangulatePolygonFan (const NCollection_Array1& theIndices) +{ + const Standard_Integer aNbElemNodes = theIndices.Size(); + for (Standard_Integer aNodeIter = 0; aNodeIter < aNbElemNodes - 2; ++aNodeIter) + { + Graphic3d_Vec4i aTriNodes (-1, -1, -1, -1); + for (Standard_Integer aNodeInSubTriIter = 0; aNodeInSubTriIter < 3; ++aNodeInSubTriIter) + { + const Standard_Integer aCurrNodeIndex = (aNodeInSubTriIter == 0) ? 0 : (aNodeIter + aNodeInSubTriIter); + aTriNodes[aNodeInSubTriIter] = theIndices.Value (theIndices.Lower() + aCurrNodeIndex); + } + addElement (aTriNodes[0], aTriNodes[1], aTriNodes[2], -1); + } + return aNbElemNodes - 2; +} + +//================================================================ +// Function : polygonCenter +// Purpose : +//================================================================ +gp_XYZ RWObj_Reader::polygonCenter (const NCollection_Array1& theIndices) +{ + if (theIndices.Size() < 3) + { + return gp_XYZ (0.0, 0.0, 0.0); + } + else if (theIndices.Size() == 4) + { + gp_XYZ aCenter = getNode (theIndices.Value (theIndices.Lower() + 0)).XYZ() + + getNode (theIndices.Value (theIndices.Lower() + 2)).XYZ(); + aCenter /= 2.0; + return aCenter; + } + + gp_XYZ aCenter (0, 0, 0); + for (NCollection_Array1::Iterator aPntIter (theIndices); aPntIter.More(); aPntIter.Next()) + { + aCenter += getNode (aPntIter.Value()).XYZ(); + } + + aCenter /= (Standard_Real )theIndices.Size(); + return aCenter; +} + +//================================================================ +// Function : polygonNormal +// Purpose : +//================================================================ +gp_XYZ RWObj_Reader::polygonNormal (const NCollection_Array1& theIndices) +{ + const gp_XYZ aCenter = polygonCenter (theIndices); + gp_XYZ aMaxDir = getNode (theIndices.First()).XYZ() - aCenter; + gp_XYZ aNormal = (getNode (theIndices.Last()).XYZ() - aCenter).Crossed (aMaxDir); + for (int aPntIter = theIndices.Lower(); aPntIter < theIndices.Upper(); ++aPntIter) + { + const gp_XYZ aTmpDir2 = getNode (theIndices.Value (aPntIter + 1)).XYZ() - aCenter; + if (aTmpDir2.SquareModulus() > aMaxDir.SquareModulus()) + { + aMaxDir = aTmpDir2; + } + + const gp_XYZ aTmpDir1 = getNode (theIndices.Value (aPntIter)).XYZ() - aCenter; + gp_XYZ aDelta = aTmpDir1.Crossed (aTmpDir2); + if (aNormal.Dot (aDelta) < 0.0) + { + aDelta *= -1.0; + } + aNormal += aDelta; + } + + const Standard_Real aMod = aNormal.Modulus(); + if (aMod > gp::Resolution()) + { + aNormal /= aMod; + } + return aNormal; +} + +//================================================================ +// Function : triangulatePolygon +// Purpose : +//================================================================ +Standard_Integer RWObj_Reader::triangulatePolygon (const NCollection_Array1& theIndices) +{ + const Standard_Integer aNbElemNodes = theIndices.Size(); + if (aNbElemNodes < 3) + { + return 0; + } + + const gp_XYZ aPolygonNorm = polygonNormal (theIndices); + + // map polygon onto plane + gp_XYZ aXDir; + { + const double aAbsXYZ[] = { Abs(aPolygonNorm.X()), Abs(aPolygonNorm.Y()), Abs(aPolygonNorm.Z()) }; + Standard_Integer aMinI = (aAbsXYZ[0] < aAbsXYZ[1]) ? 0 : 1; + aMinI = (aAbsXYZ[aMinI] < aAbsXYZ[2]) ? aMinI : 2; + const Standard_Integer aI1 = (aMinI + 1) % 3 + 1; + const Standard_Integer aI2 = (aMinI + 2) % 3 + 1; + aXDir.ChangeCoord (aMinI + 1) = 0; + aXDir.ChangeCoord (aI1) = aPolygonNorm.Coord (aI2); + aXDir.ChangeCoord (aI2) = -aPolygonNorm.Coord (aI1); + } + const gp_XYZ aYDir = aPolygonNorm ^ aXDir; + + Handle(NCollection_IncAllocator) anAllocator = new NCollection_IncAllocator(); + Handle(BRepMesh_DataStructureOfDelaun) aMeshStructure = new BRepMesh_DataStructureOfDelaun (anAllocator); + IMeshData::VectorOfInteger anIndexes (aNbElemNodes, anAllocator); + for (Standard_Integer aNodeIter = 0; aNodeIter < aNbElemNodes; ++aNodeIter) + { + const Standard_Integer aNodeIndex = theIndices.Value (theIndices.Lower() + aNodeIter); + const gp_XYZ aPnt3d = getNode (aNodeIndex).XYZ(); + gp_XY aPnt2d (aXDir * aPnt3d, aYDir * aPnt3d); + BRepMesh_Vertex aVertex (aPnt2d, aNodeIndex, BRepMesh_Frontier); + anIndexes.Append (aMeshStructure->AddNode (aVertex)); + } + + const bool isClockwiseOrdered = isClockwisePolygon (aMeshStructure, anIndexes); + for (Standard_Integer aIdx = anIndexes.Lower(); aIdx <= anIndexes.Upper(); ++aIdx) + { + const Standard_Integer aPtIdx = isClockwiseOrdered ? aIdx : (aIdx + 1) % anIndexes.Length(); + const Standard_Integer aNextPtIdx = isClockwiseOrdered ? (aIdx + 1) % anIndexes.Length() : aIdx; + BRepMesh_Edge anEdge (anIndexes.Value (aPtIdx), + anIndexes.Value (aNextPtIdx), + BRepMesh_Frontier); + aMeshStructure->AddLink (anEdge); + } + + try + { + BRepMesh_Delaun aTriangulation (aMeshStructure, anIndexes); + const IMeshData::MapOfInteger& aTriangles = aMeshStructure->ElementsOfDomain(); + if (aTriangles.Extent() < 1) + { + return triangulatePolygonFan (theIndices); + } + + Standard_Integer aNbTrisAdded = 0; + for (IMeshData::MapOfInteger::Iterator aTriIter (aTriangles); aTriIter.More(); aTriIter.Next()) + { + const Standard_Integer aTriangleId = aTriIter.Key(); + const BRepMesh_Triangle& aTriangle = aMeshStructure->GetElement (aTriangleId); + if (aTriangle.Movability() == BRepMesh_Deleted) + { + continue; + } + + int aTri2d[3]; + aMeshStructure->ElementNodes (aTriangle, aTri2d); + if (!isClockwiseOrdered) + { + std::swap (aTri2d[1], aTri2d[2]); + } + const BRepMesh_Vertex& aVertex1 = aMeshStructure->GetNode (aTri2d[0]); + const BRepMesh_Vertex& aVertex2 = aMeshStructure->GetNode (aTri2d[1]); + const BRepMesh_Vertex& aVertex3 = aMeshStructure->GetNode (aTri2d[2]); + addElement (aVertex1.Location3d(), aVertex2.Location3d(), aVertex3.Location3d(), -1); + ++aNbTrisAdded; + } + return aNbTrisAdded; + } + catch (Standard_Failure const& theFailure) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString ("Error: exception raised during polygon split\n[") + + theFailure.GetMessageString() + "]", Message_Warning); + } + return triangulatePolygonFan (theIndices); +} + +// ======================================================================= +// function : pushObject +// purpose : +// ======================================================================= +void RWObj_Reader::pushObject (const char* theObjectName) +{ + TCollection_AsciiString aNewObject; + if (!RWObj_Tools::ReadName (theObjectName, aNewObject)) + { + // empty group name is OK + } + if (addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewObject)) + { + myPackedIndices.Clear(); // vertices might be duplicated after this point... + } + myActiveSubMesh.Object = aNewObject; +} + +// ======================================================================= +// function : pushGroup +// purpose : +// ======================================================================= +void RWObj_Reader::pushGroup (const char* theGroupName) +{ + TCollection_AsciiString aNewGroup; + if (!RWObj_Tools::ReadName (theGroupName, aNewGroup)) + { + // empty group name is OK + } + if (addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewGroup)) + { + myPackedIndices.Clear(); // vertices might be duplicated after this point... + } + myActiveSubMesh.Group = aNewGroup; +} + +// ======================================================================= +// function : pushSmoothGroup +// purpose : +// ======================================================================= +void RWObj_Reader::pushSmoothGroup (const char* theSmoothGroupIndex) +{ + TCollection_AsciiString aNewSmoothGroup; + RWObj_Tools::ReadName (theSmoothGroupIndex, aNewSmoothGroup); + if (aNewSmoothGroup == "off" + || aNewSmoothGroup == "0") + { + aNewSmoothGroup.Clear(); + } + if (addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewSmoothGroup)) + { + myPackedIndices.Clear(); // vertices might be duplicated after this point... + } + myActiveSubMesh.SmoothGroup = aNewSmoothGroup; +} + +// ======================================================================= +// function : pushMaterial +// purpose : +// ======================================================================= +void RWObj_Reader::pushMaterial (const char* theMaterialName) +{ + TCollection_AsciiString aNewMat; + if (!RWObj_Tools::ReadName (theMaterialName, aNewMat)) + { + // empty material name is allowed by specs + } + else if (!myMaterials.IsBound (aNewMat)) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: use of undefined OBJ material at line ") + + myNbLines, Message_Warning); + return; + } + if (myActiveSubMesh.Material.IsEqual (aNewMat)) + { + return; // ignore + } + + // implicitly create a new group to split materials + if (addMesh (myActiveSubMesh, RWObj_SubMeshReason_NewMaterial)) + { + myPackedIndices.Clear(); // vertices might be duplicated after this point... + } + myActiveSubMesh.Material = aNewMat; +} + +// ======================================================================= +// function : readMaterialLib +// purpose : +// ======================================================================= +void RWObj_Reader::readMaterialLib (const char* theFileName) +{ + TCollection_AsciiString aMatPath; + if (!RWObj_Tools::ReadName (theFileName, aMatPath)) + { + Message::DefaultMessenger()->Send (TCollection_AsciiString("Warning: invalid OBJ syntax at line ") + + myNbLines, Message_Warning); + return; + } + + RWObj_MtlReader aMatReader (myMaterials); + if (aMatReader.Read (myFolder, aMatPath)) + { + myExternalFiles.Add (myFolder + aMatPath); + } +} + +// ======================================================================= +// function : checkMemory +// purpose : +// ======================================================================= +bool RWObj_Reader::checkMemory() +{ + if (myMemEstim < myMemLimitBytes + || myToAbort) + { + return true; + } + + Message::DefaultMessenger()->Send (TCollection_AsciiString("Error: OBJ file content does not fit into ") + + Standard_Integer(myMemLimitBytes / (1024 * 1024)) + " MiB limit." + + "\nMesh data will be truncated.", Message_Fail); + myToAbort = true; + return false; +} diff --git a/src/RWObj/RWObj_Reader.hxx b/src/RWObj/RWObj_Reader.hxx new file mode 100644 index 0000000000..b6c93b3c46 --- /dev/null +++ b/src/RWObj/RWObj_Reader.hxx @@ -0,0 +1,364 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +//! 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(); + + //! 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_Boolean Read (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress) + { + return read (theFile, theProgress, Standard_False); + } + + //! 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 theFile path to the file + //! @param theProgress progress indicator + //! @return TRUE if success, FALSE on error or user break. + //! @sa FileComments(), ExternalFiles(), NbProbeNodes(), NbProbeElems(). + Standard_Boolean Probe (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress) + { + return read (theFile, theProgress, Standard_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. + Standard_Integer NbProbeNodes() const { return myNbProbeNodes; } + + //!< number of probed polygon elements (of unknown size). + Standard_Integer NbProbeElems() const { return myNbProbeElems; } + + //! Returns memory limit in bytes; -1 (no limit) by default. + Standard_Size 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 (Standard_Size 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. + Standard_Boolean IsSinglePrecision() const { return myObjVerts.IsSinglePrecision(); } + + //! Setup single/double precision flag for reading vertex data (coordinates). + void SetSinglePrecision (Standard_Boolean 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 Standard_Boolean read (const TCollection_AsciiString& theFile, + const Handle(Message_ProgressIndicator)& theProgress, + const Standard_Boolean 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 Standard_Boolean addMesh (const RWObj_SubMesh& theMesh, + const RWObj_SubMeshReason theReason) = 0; + + //! Retrieve sub-mesh node position, added by addNode(). + virtual gp_Pnt getNode (Standard_Integer 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 Standard_Integer 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 Standard_Integer theIndex, + const Graphic3d_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 Standard_Integer theIndex, + const Graphic3d_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 (Standard_Integer theN1, + Standard_Integer theN2, + Standard_Integer theN3, + Standard_Integer theN4) = 0; + +//! @name implementation details +private: + + //! Handle "v X Y Z". + void pushVertex (const char* theXYZ) + { + char* aNext = NULL; + gp_Pnt anXYZ; + RWObj_Tools::ReadVec3 (theXYZ, aNext, anXYZ.ChangeCoord()); + myCSTrsf.TransformPosition (anXYZ.ChangeCoord()); + + myMemEstim += myObjVerts.IsSinglePrecision() ? sizeof(Graphic3d_Vec3) : sizeof(gp_Pnt); + myObjVerts.Append (anXYZ); + } + + //! Handle "vn NX NY NZ". + void pushNormal (const char* theXYZ) + { + char* aNext = NULL; + Graphic3d_Vec3 aNorm; + RWObj_Tools::ReadVec3 (theXYZ, aNext, aNorm); + myCSTrsf.TransformNormal (aNorm); + + myMemEstim += sizeof(Graphic3d_Vec3); + myObjNorms.Append (aNorm); + } + + //! Handle "vt U V". + void pushTexel (const char* theUV) + { + char* aNext = NULL; + Graphic3d_Vec2 anUV; + anUV.x() = (float )Strtod (theUV, &aNext); + theUV = aNext; + anUV.y() = (float )Strtod (theUV, &aNext); + + myMemEstim += sizeof(Graphic3d_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 + Standard_Integer triangulatePolygonFan (const NCollection_Array1& theIndices); + + //! Triangulate specified polygon. + //! @param theIndices polygon nodes + //! @return number of added triangles + Standard_Integer 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 + { + static Standard_Integer HashCode (const Graphic3d_Vec3i& theKey, + const Standard_Integer theUpper) + { + return ::HashCode (::HashCodes ((Standard_CString )&theKey, sizeof(Graphic3d_Vec3i)), theUpper); + } + + static Standard_Boolean IsEqual (const Graphic3d_Vec3i& theKey1, + const Graphic3d_Vec3i& theKey2) + { + 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 (Standard_False) {} + + //! Return single precision flag; FALSE by default. + bool IsSinglePrecision() const { return myIsSinglePrecision; } + + //! Setup single/double precision flag. + void SetSinglePrecision (Standard_Boolean 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. + Standard_Integer Lower() const { return 0; } + + //! Return vector upper index. + Standard_Integer Upper() const { return myIsSinglePrecision ? myVec3Vec->Upper() : myPntVec->Upper(); } + + //! Return point with the given index. + gp_Pnt Value (Standard_Integer theIndex) const + { + if (myIsSinglePrecision) + { + const Graphic3d_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 (Graphic3d_Vec3 ((float )thePnt.X(), (float )thePnt.Y(), (float )thePnt.Z())); + } + else + { + myPntVec->Append (thePnt); + } + } + private: + Handle(NCollection_Shared >) myPntVec; + Handle(NCollection_Shared >) myVec3Vec; + Standard_Boolean 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 + Standard_Size myMemLimitBytes; //!< memory limit in bytes + Standard_Size myMemEstim; //!< estimated memory occupation in bytes + Standard_Integer myNbLines; //!< number of parsed lines (e.g. current line) + Standard_Integer myNbProbeNodes; //!< number of probed nodes + Standard_Integer myNbProbeElems; //!< number of probed elements + Standard_Integer myNbElemsBig; //!< number of big elements (polygons with 5+ nodes) + Standard_Boolean myToAbort; //!< flag indicating abort state (e.g. syntax error) + + // 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_Vector myObjVertsUV; //!< temporary vector of UV parameters + NCollection_Vector myObjNorms; //!< temporary vector of normals + NCollection_DataMap + myPackedIndices; + NCollection_DataMap + myMaterials; //!< map of known materials + + RWObj_SubMesh myActiveSubMesh; //!< active sub-mesh definition + std::vector myCurrElem; //!< indices for the current element + +}; + +#endif // _RWObj_Reader_HeaderFile diff --git a/src/RWObj/RWObj_SubMesh.hxx b/src/RWObj/RWObj_SubMesh.hxx new file mode 100644 index 0000000000..7a960939d5 --- /dev/null +++ b/src/RWObj/RWObj_SubMesh.hxx @@ -0,0 +1,30 @@ +// 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_SubMesh_HeaderFile +#define _RWObj_SubMesh_HeaderFile + +#include +#include + +//! Sub-mesh definition for OBJ reader. +struct RWObj_SubMesh +{ + TCollection_AsciiString Object; //!< name of active object + TCollection_AsciiString Group; //!< name of active group + TCollection_AsciiString SmoothGroup; //!< name of active smoothing group + TCollection_AsciiString Material; //!< name of active material +}; + +#endif // _RWObj_SubMesh_HeaderFile diff --git a/src/RWObj/RWObj_SubMeshReason.hxx b/src/RWObj/RWObj_SubMeshReason.hxx new file mode 100644 index 0000000000..217dc34038 --- /dev/null +++ b/src/RWObj/RWObj_SubMeshReason.hxx @@ -0,0 +1,27 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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_SubMeshReason_HeaderFile +#define _RWObj_SubMeshReason_HeaderFile + +//! Reason for creating a new group within OBJ reader. +enum RWObj_SubMeshReason +{ + RWObj_SubMeshReason_NewObject, //!< new object, should occur only ones in valid OBJ file (at the very beginning) + RWObj_SubMeshReason_NewGroup, //!< new group (g item) + RWObj_SubMeshReason_NewMaterial, //!< new material (usemtl item) + RWObj_SubMeshReason_NewSmoothGroup //!< new smoothing group (s item) +}; + +#endif // _RWObj_SubMeshReason_HeaderFile diff --git a/src/RWObj/RWObj_Tools.hxx b/src/RWObj/RWObj_Tools.hxx new file mode 100644 index 0000000000..d105b1c17b --- /dev/null +++ b/src/RWObj/RWObj_Tools.hxx @@ -0,0 +1,81 @@ +// Author: Kirill Gavrilov +// Copyright (c) 2017-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_Tools_HeaderFile +#define _RWObj_Tools_HeaderFile + +#include +#include +#include + +//! Auxiliary tools for OBJ format parser. +namespace RWObj_Tools +{ + //! Read 3 float values. + inline bool ReadVec3 (const char* thePos, + char*& theNext, + Graphic3d_Vec3& theVec) + { + const char* aPos = thePos; + theVec.x() = (float )Strtod (aPos, &theNext); + aPos = theNext; + theVec.y() = (float )Strtod (aPos, &theNext); + aPos = theNext; + theVec.z() = (float )Strtod (aPos, &theNext); + return aPos != theNext; + } + + //! Read 3 double values. + inline bool ReadVec3 (const char* thePos, + char*& theNext, + gp_XYZ& theVec) + { + const char* aPos = thePos; + theVec.SetX (Strtod (aPos, &theNext)); + aPos = theNext; + theVec.SetY (Strtod (aPos, &theNext)); + aPos = theNext; + theVec.SetZ (Strtod (aPos, &theNext)); + return aPos != theNext; + } + + //! Read string. + inline bool ReadName (const char* thePos, + TCollection_AsciiString& theName) + { + Standard_Integer aFrom = 0; + Standard_Integer aTail = (Standard_Integer )std::strlen (thePos) - 1; + if (aTail >= 0 && thePos[aTail] == '\n') { --aTail; } + if (aTail >= 0 && thePos[aTail] == '\r') { --aTail; } + for (; aTail >= 0 && IsSpace (thePos[aTail]); --aTail) {} // RightAdjust + for (; aFrom < aTail && IsSpace (thePos[aFrom]); ++aFrom) {} // LeftAdjust + if (aFrom > aTail) + { + theName.Clear(); + return false; + } + theName = TCollection_AsciiString (thePos + aFrom, aTail - aFrom + 1); + return true; + } + + //! Return true if specified char is a white space. + inline bool isSpaceChar (const char theChar) + { + return theChar == ' ' + || theChar == '\t'; + //return IsSpace (theChar); + } +} + +#endif // _RWObj_Tools_HeaderFile diff --git a/src/RWObj/RWObj_TriangulationReader.cxx b/src/RWObj/RWObj_TriangulationReader.cxx new file mode 100644 index 0000000000..1c4b7031ee --- /dev/null +++ b/src/RWObj/RWObj_TriangulationReader.cxx @@ -0,0 +1,223 @@ +// Author: Kirill Gavrilov +// Copyright (c) 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. + +#include + +#include +#include +#include + +IMPLEMENT_STANDARD_RTTIEXT(RWObj_TriangulationReader, RWObj_Reader) + +//================================================================ +// Function : addMesh +// Purpose : +//================================================================ +Standard_Boolean RWObj_TriangulationReader::addMesh (const RWObj_SubMesh& theMesh, + const RWObj_SubMeshReason theReason) +{ + if (!myToCreateShapes) + { + return Standard_False; + } + + const RWObj_Material* aMaterial = myMaterials.Seek (theMesh.Material); + if (Handle(Poly_Triangulation) aTris = GetTriangulation()) + { + myNodes.Clear(); + myNodesUV.Clear(); + myNormals.Clear(); + myTriangles.Clear(); + if (theMesh.Group != myLastGroupName) + { + // flush previous group and start a new one + if (addSubShape (myLastObjectShape, myLastGroupShape, Standard_False)) + { + if (myShapeReceiver != NULL) + { + myShapeReceiver->BindNamedShape (myLastGroupShape, theMesh.Group, myLastGroupShape.ShapeType() == TopAbs_FACE ? aMaterial : NULL, Standard_False); + } + } + myLastGroupShape = TopoDS_Shape(); + myLastGroupName = theMesh.Group; + } + + TopoDS_Face aNewFace; + BRep_Builder aBuilder; + aBuilder.MakeFace (aNewFace, aTris); + addSubShape (myLastGroupShape, aNewFace, Standard_True); + if (myShapeReceiver != NULL) + { + myShapeReceiver->BindNamedShape (aNewFace, "", aMaterial, Standard_False); + } + } + + if (theReason == RWObj_SubMeshReason_NewObject) + { + // forced flush at the end of the object + if (addSubShape (myLastObjectShape, myLastGroupShape, Standard_False)) + { + if (myShapeReceiver != NULL) + { + myShapeReceiver->BindNamedShape (myLastGroupShape, theMesh.Group, myLastGroupShape.ShapeType() == TopAbs_FACE ? aMaterial : NULL, Standard_False); + } + } + myLastGroupShape = TopoDS_Shape(); + myLastGroupName.Clear(); + + if (addSubShape (myResultShape, myLastObjectShape, Standard_False)) + { + if (myShapeReceiver != NULL) + { + myShapeReceiver->BindNamedShape (myLastObjectShape, theMesh.Object, NULL, Standard_True); + } + } + myLastObjectShape = TopoDS_Compound(); + } + return Standard_True; +} + +// ======================================================================= +// function : addSubShape +// purpose : +// ======================================================================= +Standard_Boolean RWObj_TriangulationReader::addSubShape (TopoDS_Shape& theParent, + const TopoDS_Shape& theSubShape, + const Standard_Boolean theToExpandCompound) +{ + if (theSubShape.IsNull()) + { + return Standard_False; + } + + BRep_Builder aBuilder; + if (theParent.IsNull() + && theToExpandCompound) + { + theParent = theSubShape; + return Standard_True; + } + + TopoDS_Compound aComp; + if (!theParent.IsNull() + && theParent.ShapeType() == TopAbs_COMPOUND) + { + aComp = TopoDS::Compound (theParent); + } + else + { + aBuilder.MakeCompound (aComp); + if (!theParent.IsNull()) + { + aBuilder.Add (aComp, theParent); + } + } + aBuilder.Add (aComp, theSubShape); + theParent = aComp; + return Standard_True; +} + +//============================================================================= +//function : GetTriangulation +//purpose : +//============================================================================= +Handle(Poly_Triangulation) RWObj_TriangulationReader::GetTriangulation() +{ + if (myTriangles.IsEmpty()) + { + return Handle(Poly_Triangulation)(); + } + + const Standard_Boolean hasNormals = myNodes.Length() == myNormals.Length(); + const Standard_Boolean hasUV = myNodes.Length() == myNodesUV.Length(); + + Handle(Poly_Triangulation) aPoly = new Poly_Triangulation (myNodes.Length(), myTriangles.Length(), hasUV); + for (Standard_Integer aNodeIter = 0; aNodeIter < myNodes.Size(); ++aNodeIter) + { + const gp_Pnt& aNode = myNodes.Value (aNodeIter); + aPoly->ChangeNode (aNodeIter + 1) = aNode; + } + if (hasUV) + { + for (Standard_Integer aNodeIter = 0; aNodeIter < myNodes.Size(); ++aNodeIter) + { + const Graphic3d_Vec2& aNode = myNodesUV.Value (aNodeIter); + aPoly->ChangeUVNode (aNodeIter + 1).SetCoord (aNode.x(), aNode.y()); + } + } + if (hasNormals) + { + const Handle(TShort_HArray1OfShortReal) aNormals = new TShort_HArray1OfShortReal (1, myNodes.Length() * 3); + Standard_ShortReal* aNormArr = &aNormals->ChangeFirst(); + Standard_Integer aNbInvalid = 0; + for (Standard_Integer aNodeIter = 0; aNodeIter < myNodes.Size(); ++aNodeIter) + { + const Graphic3d_Vec3& aNorm = myNormals.Value (aNodeIter); + const float aMod2 = aNorm.SquareModulus(); + if (aMod2 > 0.001f) + { + aNormArr[aNodeIter * 3 + 0] = aNorm.x(); + aNormArr[aNodeIter * 3 + 1] = aNorm.y(); + aNormArr[aNodeIter * 3 + 2] = aNorm.z(); + } + else + { + ++aNbInvalid; + aNormArr[aNodeIter * 3 + 0] = 0.0f; + aNormArr[aNodeIter * 3 + 1] = 0.0f; + aNormArr[aNodeIter * 3 + 2] = 1.0f; + } + } + if (aNbInvalid != myNodes.Length()) + { + aPoly->SetNormals (aNormals); + } + } + + for (Standard_Integer aTriIter = 0; aTriIter < myTriangles.Size(); ++aTriIter) + { + aPoly->ChangeTriangle (aTriIter + 1) = myTriangles (aTriIter); + } + + return aPoly; +} + +//================================================================ +// Function : ResultShape +// Purpose : +//================================================================ +TopoDS_Shape RWObj_TriangulationReader::ResultShape() +{ + if (!myToCreateShapes) + { + if (Handle(Poly_Triangulation) aTris = GetTriangulation()) + { + TopoDS_Face aFace; + BRep_Builder aBuilder; + aBuilder.MakeFace (aFace, aTris); + return aFace; + } + return TopoDS_Shape(); + } + + if (!myResultShape.IsNull() + && myResultShape.ShapeType() == TopAbs_COMPOUND + && myResultShape.NbChildren() == 1 + && myActiveSubMesh.Object.IsEmpty()) + { + TopoDS_Iterator aChildIter (myResultShape); + return aChildIter.Value(); + } + return myResultShape; +} diff --git a/src/RWObj/RWObj_TriangulationReader.hxx b/src/RWObj/RWObj_TriangulationReader.hxx new file mode 100644 index 0000000000..da51d1e73d --- /dev/null +++ b/src/RWObj/RWObj_TriangulationReader.hxx @@ -0,0 +1,124 @@ +// Author: Kirill Gavrilov +// Copyright (c) 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_TriangulationReader_HeaderFile +#define _RWObj_TriangulationReader_HeaderFile + +#include + +#include +#include + +//! Interface to store shape attributes into document. +class RWObj_IShapeReceiver +{ +public: + //! @param theShape shape to register + //! @param theName shape name + //! @param theMaterial shape material + //! @param theIsRootShape indicates that this is a root object (free shape) + virtual void BindNamedShape (const TopoDS_Shape& theShape, + const TCollection_AsciiString& theName, + const RWObj_Material* theMaterial, + const Standard_Boolean theIsRootShape) = 0; +}; + +//! RWObj_Reader implementation dumping OBJ file into Poly_Triangulation. +class RWObj_TriangulationReader : public RWObj_Reader +{ + DEFINE_STANDARD_RTTIEXT(RWObj_TriangulationReader, RWObj_Reader) +public: + + //! Constructor. + RWObj_TriangulationReader() : myShapeReceiver (NULL), myToCreateShapes (Standard_True) {} + + //! Set flag to create shapes. + void SetCreateShapes (Standard_Boolean theToCreateShapes) { myToCreateShapes = theToCreateShapes; } + + //! Set shape receiver callback. + void SetShapeReceiver (RWObj_IShapeReceiver* theReceiver) { myShapeReceiver = theReceiver; } + + //! Create Poly_Triangulation from collected data + Standard_EXPORT virtual Handle(Poly_Triangulation) GetTriangulation(); + + //! Return result shape. + Standard_EXPORT TopoDS_Shape ResultShape(); + +protected: + + //! Flush active sub-mesh. + Standard_EXPORT virtual Standard_Boolean addMesh (const RWObj_SubMesh& theMesh, + const RWObj_SubMeshReason theReason) Standard_OVERRIDE; + + //! Retrieve sub-mesh node position. + virtual gp_Pnt getNode (Standard_Integer theIndex) const Standard_OVERRIDE + { + return myNodes.Value (theIndex - 1); + } + + //! Add new node. + virtual Standard_Integer addNode (const gp_Pnt& thePnt) Standard_OVERRIDE + { + myNodes.Append (thePnt); + return myNodes.Size(); + } + + //! Ignore normal. + virtual void setNodeNormal (const Standard_Integer theIndex, + const Graphic3d_Vec3& theNormal) Standard_OVERRIDE + { + myNormals.SetValue (theIndex - 1, theNormal); + } + + //! Ignore texture coordinates. + virtual void setNodeUV (const Standard_Integer theIndex, + const Graphic3d_Vec2& theUV) Standard_OVERRIDE + { + myNodesUV.SetValue (theIndex - 1, theUV); + } + + //! Add element. + virtual void addElement (Standard_Integer theN1, Standard_Integer theN2, Standard_Integer theN3, Standard_Integer theN4) Standard_OVERRIDE + { + myTriangles.Append (Poly_Triangle (theN1, theN2, theN3)); + if (theN4 != -1) + { + myTriangles.Append (Poly_Triangle (theN1, theN3, theN4)); + } + } + +protected: + + //! Add sub-shape into specified shape + Standard_EXPORT Standard_Boolean addSubShape (TopoDS_Shape& theParent, + const TopoDS_Shape& theSubShape, + const Standard_Boolean theToExpandCompound); + +protected: + + NCollection_Vector myNodes; //!< nodes of currently filled triangulation + NCollection_Vector myNormals; //!< normals of currently filled triangulation + NCollection_Vector myNodesUV; //!< UVs of currently filled triangulation + NCollection_Vector myTriangles; //!< indexes of currently filled triangulation + + RWObj_IShapeReceiver* myShapeReceiver; //!< optional shape receiver + TopoDS_Compound myResultShape; //!< result shape as Compound of objects + TopoDS_Compound myLastObjectShape; //!< Compound containing current object groups + TopoDS_Shape myLastGroupShape; //!< current group shape - either a single Face or Compound of Faces + TCollection_AsciiString myLastGroupName; //!< current group name + Standard_Boolean myToCreateShapes; //!< create a single triangulation + +}; + +#endif // _RWObj_TriangulationReader_HeaderFile diff --git a/src/TKRWMesh/PACKAGES b/src/TKRWMesh/PACKAGES index 5bfd2a348f..2296ef2c2e 100644 --- a/src/TKRWMesh/PACKAGES +++ b/src/TKRWMesh/PACKAGES @@ -1,2 +1,3 @@ RWGltf RWMesh +RWObj diff --git a/src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx b/src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx index 3bcade2ca0..5a0551d4b5 100644 --- a/src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx +++ b/src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx @@ -44,6 +44,8 @@ #include #include #include +#include +#include #include #include #include @@ -290,6 +292,210 @@ static Standard_Integer readstl(Draw_Interpretor& theDI, return 0; } +//! Parse RWMesh_CoordinateSystem enumeration. +static Standard_Boolean parseCoordinateSystem (const char* theArg, + RWMesh_CoordinateSystem& theSystem) +{ + TCollection_AsciiString aCSStr (theArg); + aCSStr.LowerCase(); + if (aCSStr == "zup") + { + theSystem = RWMesh_CoordinateSystem_Zup; + } + else if (aCSStr == "yup") + { + theSystem = RWMesh_CoordinateSystem_Yup; + } + else + { + return Standard_False; + } + return Standard_True; +} + +//============================================================================= +//function : ReadObj +//purpose : Reads OBJ file +//============================================================================= +static Standard_Integer ReadObj (Draw_Interpretor& theDI, + Standard_Integer theNbArgs, + const char** theArgVec) +{ + TCollection_AsciiString aDestName, aFilePath; + Standard_Boolean toUseExistingDoc = Standard_False; + Standard_Real aFileUnitFactor = -1.0; + RWMesh_CoordinateSystem aResultCoordSys = RWMesh_CoordinateSystem_Zup, aFileCoordSys = RWMesh_CoordinateSystem_Yup; + Standard_Boolean toListExternalFiles = Standard_False, isSingleFace = Standard_False, isSinglePrecision = Standard_False; + Standard_Boolean isNoDoc = (TCollection_AsciiString(theArgVec[0]) == "readobj"); + for (Standard_Integer anArgIter = 1; anArgIter < theNbArgs; ++anArgIter) + { + TCollection_AsciiString anArgCase (theArgVec[anArgIter]); + anArgCase.LowerCase(); + if (anArgIter + 1 < theNbArgs + && (anArgCase == "-unit" + || anArgCase == "-units" + || anArgCase == "-fileunit" + || anArgCase == "-fileunits")) + { + const TCollection_AsciiString aUnitStr (theArgVec[++anArgIter]); + aFileUnitFactor = UnitsAPI::AnyToSI (1.0, aUnitStr.ToCString()); + if (aFileUnitFactor <= 0.0) + { + std::cout << "Syntax error: wrong length unit '" << aUnitStr << "'\n"; + return 1; + } + } + else if (anArgIter + 1 < theNbArgs + && (anArgCase == "-filecoordinatesystem" + || anArgCase == "-filecoordsystem" + || anArgCase == "-filecoordsys")) + { + if (!parseCoordinateSystem (theArgVec[++anArgIter], aFileCoordSys)) + { + std::cout << "Syntax error: unknown coordinate system '" << theArgVec[anArgIter] << "'\n"; + return 1; + } + } + else if (anArgIter + 1 < theNbArgs + && (anArgCase == "-resultcoordinatesystem" + || anArgCase == "-resultcoordsystem" + || anArgCase == "-resultcoordsys" + || anArgCase == "-rescoordsys")) + { + if (!parseCoordinateSystem (theArgVec[++anArgIter], aResultCoordSys)) + { + std::cout << "Syntax error: unknown coordinate system '" << theArgVec[anArgIter] << "'\n"; + return 1; + } + } + else if (anArgCase == "-singleprecision" + || anArgCase == "-singleprec") + { + isSinglePrecision = Standard_True; + if (anArgIter + 1 < theNbArgs + && ViewerTest::ParseOnOff (theArgVec[anArgIter + 1], isSinglePrecision)) + { + ++anArgIter; + } + } + else if (isNoDoc + && (anArgCase == "-singleface" + || anArgCase == "-singletriangulation")) + { + isSingleFace = Standard_True; + } + else if (!isNoDoc + && (anArgCase == "-nocreate" + || anArgCase == "-nocreatedoc")) + { + toUseExistingDoc = Standard_True; + if (anArgIter + 1 < theNbArgs + && ViewerTest::ParseOnOff (theArgVec[anArgIter + 1], toUseExistingDoc)) + { + ++anArgIter; + } + } + else if (anArgCase == "-listexternalfiles" + || anArgCase == "-listexternals" + || anArgCase == "-listexternal" + || anArgCase == "-external" + || anArgCase == "-externalfiles") + { + toListExternalFiles = Standard_True; + } + else if (aDestName.IsEmpty()) + { + aDestName = theArgVec[anArgIter]; + } + else if (aFilePath.IsEmpty()) + { + aFilePath = theArgVec[anArgIter]; + } + else + { + std::cout << "Syntax error at '" << theArgVec[anArgIter] << "'\n"; + return 1; + } + } + if (aFilePath.IsEmpty()) + { + std::cout << "Syntax error: wrong number of arguments\n"; + return 1; + } + + Handle(Draw_ProgressIndicator) aProgress = new Draw_ProgressIndicator (theDI, 1); + Handle(TDocStd_Document) aDoc; + if (!isNoDoc + && !toListExternalFiles) + { + Handle(TDocStd_Application) anApp = DDocStd::GetApplication(); + Standard_CString aNameVar = aDestName.ToCString(); + DDocStd::GetDocument (aNameVar, aDoc, Standard_False); + if (aDoc.IsNull()) + { + if (toUseExistingDoc) + { + std::cout << "Error: document with name " << aDestName << " does not exist\n"; + return 1; + } + anApp->NewDocument (TCollection_ExtendedString ("BinXCAF"), aDoc); + } + else if (!toUseExistingDoc) + { + std::cout << "Error: document with name " << aDestName << " already exists\n"; + return 1; + } + } + + RWObj_CafReader aReader; + aReader.SetSinglePrecision (isSinglePrecision); + aReader.SetSystemLengthUnit (UnitsMethods::GetCasCadeLengthUnit() * 0.001); + aReader.SetSystemCoordinateSystem (aResultCoordSys); + aReader.SetFileLengthUnit (aFileUnitFactor); + aReader.SetFileCoordinateSystem (aFileCoordSys); + aReader.SetDocument (aDoc); + if (isSingleFace) + { + RWObj_TriangulationReader aSimpleReader; + aSimpleReader.SetSinglePrecision (isSinglePrecision); + aSimpleReader.SetCreateShapes (Standard_False); + aSimpleReader.SetTransformation (aReader.CoordinateSystemConverter()); + aSimpleReader.Read (aFilePath.ToCString(), aProgress); + + Handle(Poly_Triangulation) aTriangulation = aSimpleReader.GetTriangulation(); + TopoDS_Face aFace; + BRep_Builder aBuiler; + aBuiler.MakeFace (aFace); + aBuiler.UpdateFace (aFace, aTriangulation); + DBRep::Set (aDestName.ToCString(), aFace); + return 0; + } + + if (toListExternalFiles) + { + aReader.ProbeHeader (aFilePath); + for (NCollection_IndexedMap::Iterator aFileIter (aReader.ExternalFiles()); aFileIter.More(); aFileIter.Next()) + { + theDI << "\"" << aFileIter.Value() << "\" "; + } + } + else + { + aReader.Perform (aFilePath, aProgress); + if (isNoDoc) + { + DBRep::Set (aDestName.ToCString(), aReader.SingleShape()); + } + else + { + Handle(DDocStd_DrawDocument) aDrawDoc = new DDocStd_DrawDocument (aDoc); + TDataStd_Name::Set (aDoc->GetData()->Root(), aDestName.ToCString()); + Draw::Set (aDestName.ToCString(), aDrawDoc); + } + } + return 0; +} + static Standard_Integer writevrml (Draw_Interpretor& di, Standard_Integer argc, const char** argv) { @@ -1404,6 +1610,25 @@ void XSDRAWSTLVRML::InitCommands (Draw_Interpretor& theCommands) "\n\t\t: Single triangulation-only Face is created otherwise (default).", __FILE__, readstl, g); theCommands.Add ("loadvrml" , "shape file",__FILE__,loadvrml,g); + theCommands.Add ("ReadObj", + "ReadObj Doc file [-fileCoordSys {Zup|Yup}] [-fileUnit Unit]" + "\n\t\t: [-resultCoordSys {Zup|Yup}] [-singlePrecision]" + "\n\t\t: [-listExternalFiles] [-noCreateDoc]" + "\n\t\t: Read OBJ file into XDE document." + "\n\t\t: -fileUnit length unit of OBJ file content;" + "\n\t\t: -fileCoordSys coordinate system defined by OBJ file; Yup when not specified." + "\n\t\t: -resultCoordSys result coordinate system; Zup when not specified." + "\n\t\t: -singlePrecision truncate vertex data to single precision during read; FALSE by default." + "\n\t\t: -listExternalFiles do not read mesh and only list external files." + "\n\t\t: -noCreateDoc read into existing XDE document.", + __FILE__, ReadObj, g); + theCommands.Add ("readobj", + "readobj shape file [-fileCoordSys {Zup|Yup}] [-fileUnit Unit]" + "\n\t\t: [-resultCoordSys {Zup|Yup}] [-singlePrecision]" + "\n\t\t: [-singleFace]" + "\n\t\t: Same as ReadObj but reads OBJ file into a shape instead of a document." + "\n\t\t: -singleFace merge OBJ content into a single triangulation Face.", + __FILE__, ReadObj, g); theCommands.Add ("meshfromstl", "creates MeshVS_Mesh from STL file", __FILE__, createmesh, g ); theCommands.Add ("mesh3delem", "creates 3d element mesh to test", __FILE__, create3d, g ); diff --git a/tests/de_mesh/grids.list b/tests/de_mesh/grids.list index d930f98f03..9ed1a89bc4 100644 --- a/tests/de_mesh/grids.list +++ b/tests/de_mesh/grids.list @@ -1,3 +1,4 @@ 001 stl_read 002 shape_write_stl 003 gltf_read +004 obj_read diff --git a/tests/de_mesh/obj_read/begin b/tests/de_mesh/obj_read/begin new file mode 100644 index 0000000000..a6de429d5c --- /dev/null +++ b/tests/de_mesh/obj_read/begin @@ -0,0 +1,2 @@ +pload XDE OCAF MODELING VISUALIZATION +catch { Close D } diff --git a/tests/de_mesh/obj_read/end b/tests/de_mesh/obj_read/end new file mode 100644 index 0000000000..df717a06ce --- /dev/null +++ b/tests/de_mesh/obj_read/end @@ -0,0 +1,6 @@ +vclear +vinit View1 +XDisplay -dispMode 1 D -explore +vaxo +vfit +vdump ${imagedir}/${casename}.png diff --git a/tests/de_mesh/obj_read/mustang b/tests/de_mesh/obj_read/mustang new file mode 100644 index 0000000000..bdbad5b32c --- /dev/null +++ b/tests/de_mesh/obj_read/mustang @@ -0,0 +1,9 @@ +puts "========" +puts "0029296: Data Exchange - implement import of mesh data from files in OBJ format" +puts "Small textured airplane model" +puts "========" + +ReadObj D [locate_data_file "P-51 Mustang.obj"] +XGetOneShape s D +checknbshapes s -face 14 -compound 1 +checktrinfo s -tri 4309 -nod 4727 diff --git a/tests/de_mesh/obj_read/prism b/tests/de_mesh/obj_read/prism new file mode 100644 index 0000000000..231ba454d6 --- /dev/null +++ b/tests/de_mesh/obj_read/prism @@ -0,0 +1,73 @@ +puts "========" +puts "0029296: Data Exchange - implement import of mesh data from files in OBJ format" +puts "Reading small generated file." +puts "========" + +set material_mtl {newmtl ObjMat1 +Ns 96.07 +Ka 0.00 0.00 0.00 +Kd 0.64 0.64 0.64 +Ks 0.50 0.50 0.50 +Ni 1.00 +d 1.00} + +set minimal_ascii_obj {mtllib A1_material.mtl +g Group1 +usemtl ObjMat1 +v 0 0 0 +v 2 0 0 +v 2 1 0 +v 1 2 0 +v 0 1 0 +v 0 0 2 +v 2 0 2 +v 2 1 2 +v 1 2 2 +v 0 1 2 +f 5 4 3 2 1 +f 7 8 9 10 6 +f 10 9 4 5 +f 9 8 3 4 +f 6 10 5 1 +f 2 3 8 7 +f 1 2 7 6} + +# Ascii MTL file, CRLF +set fd [open ${imagedir}/${casename}_material.mtl w] +fconfigure $fd -translation crlf +puts $fd $material_mtl +close $fd + +puts "" +puts "#======================================================================" +puts "# Ascii file, CRLF" +puts "#======================================================================" +set fd [open ${imagedir}/${casename}_one_ascii_dos.obj w] +fconfigure $fd -translation crlf +puts $fd $minimal_ascii_obj +close $fd +readobj mcrlf ${imagedir}/${casename}_one_ascii_dos.obj -singleFace +checknbshapes mcrlf -face 1 +checktrinfo mcrlf -tri 16 -nod 10 + +puts "" +puts "#======================================================================" +puts "# Ascii file with single facet, LF" +puts "#======================================================================" +set fd [open ${imagedir}/${casename}_one_ascii_unix.obj w] +fconfigure $fd -translation lf +puts $fd $minimal_ascii_obj +close $fd +readobj mlf ${imagedir}/${casename}_one_ascii_unix.obj -singleFace +checknbshapes mlf -face 1 +checktrinfo mlf -tri 16 -nod 10 + +vclear +vinit View1 +vdisplay -dispMode 1 mlf +vaxo +vfit +vdump ${imagedir}/${casename}_raw.png + +# read OBJ into document +ReadObj D ${imagedir}/${casename}_one_ascii_unix.obj diff --git a/tests/de_mesh/obj_read/ship_boat b/tests/de_mesh/obj_read/ship_boat new file mode 100644 index 0000000000..206876b8ac --- /dev/null +++ b/tests/de_mesh/obj_read/ship_boat @@ -0,0 +1,9 @@ +puts "========" +puts "0029296: Data Exchange - implement import of mesh data from files in OBJ format" +puts "Ship model with transparent windows" +puts "========" + +ReadObj D [locate_data_file ship_boat.obj] +XGetOneShape s D +checknbshapes s -face 2359 -compound 2 +checktrinfo s -tri 27297 -nod 40496 -- 2.20.1