0029296: Data Exchange - implement import of mesh data from files in OBJ format IR-2019-06-21
authorkgv <kgv@opencascade.com>
Sun, 5 May 2019 17:31:35 +0000 (20:31 +0300)
committerbugmaster <bugmaster@opencascade.com>
Fri, 21 Jun 2019 15:19:09 +0000 (18:19 +0300)
RWObj_Reader and RWObj_CafReader - added new classes reading triangulation from OBJ file.

24 files changed:
adm/UDLIST
src/RWObj/FILES [new file with mode: 0644]
src/RWObj/RWObj.cxx [new file with mode: 0644]
src/RWObj/RWObj.hxx [new file with mode: 0644]
src/RWObj/RWObj_CafReader.cxx [new file with mode: 0644]
src/RWObj/RWObj_CafReader.hxx [new file with mode: 0644]
src/RWObj/RWObj_Material.hxx [new file with mode: 0644]
src/RWObj/RWObj_MtlReader.cxx [new file with mode: 0644]
src/RWObj/RWObj_MtlReader.hxx [new file with mode: 0644]
src/RWObj/RWObj_Reader.cxx [new file with mode: 0644]
src/RWObj/RWObj_Reader.hxx [new file with mode: 0644]
src/RWObj/RWObj_SubMesh.hxx [new file with mode: 0644]
src/RWObj/RWObj_SubMeshReason.hxx [new file with mode: 0644]
src/RWObj/RWObj_Tools.hxx [new file with mode: 0644]
src/RWObj/RWObj_TriangulationReader.cxx [new file with mode: 0644]
src/RWObj/RWObj_TriangulationReader.hxx [new file with mode: 0644]
src/TKRWMesh/PACKAGES
src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx
tests/de_mesh/grids.list
tests/de_mesh/obj_read/begin [new file with mode: 0644]
tests/de_mesh/obj_read/end [new file with mode: 0644]
tests/de_mesh/obj_read/mustang [new file with mode: 0644]
tests/de_mesh/obj_read/prism [new file with mode: 0644]
tests/de_mesh/obj_read/ship_boat [new file with mode: 0644]

index 7f39f5e..634ba2d 100644 (file)
@@ -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 (file)
index 0000000..f6a988e
--- /dev/null
@@ -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 (file)
index 0000000..60731ab
--- /dev/null
@@ -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 <RWObj.hxx>
+
+#include <RWObj_TriangulationReader.hxx>
+
+//=============================================================================
+//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 (file)
index 0000000..1d5035f
--- /dev/null
@@ -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 <Message_ProgressIndicator.hxx>
+#include <OSD_Path.hxx>
+#include <Poly_Triangulation.hxx>
+#include <Standard_Macro.hxx>
+
+//! 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 (file)
index 0000000..2ff06ce
--- /dev/null
@@ -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 <RWObj_CafReader.hxx>
+
+#include <Message_ProgressSentry.hxx>
+
+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<TCollection_AsciiString>::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 (file)
index 0000000..a924a5f
--- /dev/null
@@ -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 <RWMesh_CafReader.hxx>
+#include <RWObj_TriangulationReader.hxx>
+
+//! 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 (file)
index 0000000..b47bea5
--- /dev/null
@@ -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 <Quantity_Color.hxx>
+#include <TCollection_AsciiString.hxx>
+
+//! 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 (file)
index 0000000..549d83c
--- /dev/null
@@ -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 <RWObj_MtlReader.hxx>
+
+#include <RWObj_Tools.hxx>
+
+#include <Message.hxx>
+#include <Message_Messenger.hxx>
+#include <OSD_File.hxx>
+#include <OSD_OpenFile.hxx>
+#include <OSD_Path.hxx>
+
+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<TCollection_AsciiString, RWObj_Material>& 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 (file)
index 0000000..e2d1a07
--- /dev/null
@@ -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 <Graphic3d_Vec3.hxx>
+#include <RWObj_Material.hxx>
+#include <NCollection_DataMap.hxx>
+
+//! Reader of mtl files.
+class RWObj_MtlReader
+{
+public:
+
+  //! Main constructor.
+  RWObj_MtlReader (NCollection_DataMap<TCollection_AsciiString, RWObj_Material>& 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<TCollection_AsciiString, RWObj_Material>* 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 (file)
index 0000000..0d9b74a
--- /dev/null
@@ -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 <RWObj_Reader.hxx>
+
+#include <RWObj_MtlReader.hxx>
+
+#include <BRepMesh_DataStructureOfDelaun.hxx>
+#include <BRepMesh_Delaun.hxx>
+#include <gp_XY.hxx>
+#include <Message.hxx>
+#include <Message_Messenger.hxx>
+#include <Message_ProgressSentry.hxx>
+#include <NCollection_IncAllocator.hxx>
+#include <OSD_OpenFile.hxx>
+#include <OSD_Path.hxx>
+#include <OSD_Timer.hxx>
+#include <Precision.hxx>
+#include <Standard_CLocaleSentry.hxx>
+
+#include <algorithm>
+#include <limits>
+
+#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<char> 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<TCollection_AsciiString, RWObj_Material>::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<Standard_Integer> 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<Standard_Integer>& 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<Standard_Integer>& 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<Standard_Integer>::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<Standard_Integer>& 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<Standard_Integer>& 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 (file)
index 0000000..b6c93b3
--- /dev/null
@@ -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 <gp_XYZ.hxx>
+#include <Graphic3d_Vec2.hxx>
+#include <Graphic3d_Vec4.hxx>
+#include <Message.hxx>
+#include <Message_Messenger.hxx>
+#include <Message_ProgressIndicator.hxx>
+#include <NCollection_Array1.hxx>
+#include <NCollection_DataMap.hxx>
+#include <NCollection_IndexedMap.hxx>
+#include <NCollection_Vector.hxx>
+#include <NCollection_Shared.hxx>
+
+#include <RWMesh_CoordinateSystemConverter.hxx>
+#include <RWObj_Material.hxx>
+#include <RWObj_SubMesh.hxx>
+#include <RWObj_SubMeshReason.hxx>
+#include <RWObj_Tools.hxx>
+
+#include <vector>
+
+//! 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<TCollection_AsciiString>& 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<Standard_Integer>& 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<Standard_Integer>& theIndices);
+
+  //! Create triangle fan from specified polygon.
+  //! @param theIndices polygon nodes
+  //! @return number of added triangles
+  Standard_Integer triangulatePolygonFan (const NCollection_Array1<Standard_Integer>& theIndices);
+
+  //! Triangulate specified polygon.
+  //! @param theIndices polygon nodes
+  //! @return number of added triangles
+  Standard_Integer triangulatePolygon (const NCollection_Array1<Standard_Integer>& 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<NCollection_Vector<Graphic3d_Vec3> >();
+      }
+      else
+      {
+        myPntVec = new NCollection_Shared<NCollection_Vector<gp_Pnt> >();
+      }
+    }
+
+    //! 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<NCollection_Vector<gp_Pnt> >)         myPntVec;
+    Handle(NCollection_Shared<NCollection_Vector<Graphic3d_Vec3> >) myVec3Vec;
+    Standard_Boolean myIsSinglePrecision;
+  };
+
+protected:
+
+  NCollection_IndexedMap<TCollection_AsciiString>
+                                     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<Graphic3d_Vec2> myObjVertsUV;    //!< temporary vector of UV parameters
+  NCollection_Vector<Graphic3d_Vec3> myObjNorms;      //!< temporary vector of normals
+  NCollection_DataMap<Graphic3d_Vec3i, Standard_Integer, ObjVec3iHasher>
+                                     myPackedIndices;
+  NCollection_DataMap<TCollection_AsciiString, RWObj_Material>
+                                     myMaterials;     //!< map of known materials
+
+  RWObj_SubMesh                      myActiveSubMesh; //!< active sub-mesh definition
+  std::vector<Standard_Integer>      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 (file)
index 0000000..7a96093
--- /dev/null
@@ -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 <Quantity_Color.hxx>
+#include <TCollection_AsciiString.hxx>
+
+//! 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 (file)
index 0000000..217dc34
--- /dev/null
@@ -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 (file)
index 0000000..d105b1c
--- /dev/null
@@ -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 <gp_XYZ.hxx>
+#include <Graphic3d_Vec3.hxx>
+#include <TCollection_AsciiString.hxx>
+
+//! 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 (file)
index 0000000..1c4b703
--- /dev/null
@@ -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 <RWObj_TriangulationReader.hxx>
+
+#include <BRep_Builder.hxx>
+#include <TopoDS.hxx>
+#include <TopoDS_Iterator.hxx>
+
+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 (file)
index 0000000..da51d1e
--- /dev/null
@@ -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 <RWObj_Reader.hxx>
+
+#include <Poly_Triangulation.hxx>
+#include <TopoDS_Compound.hxx>
+
+//! 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<gp_Pnt>         myNodes;     //!< nodes   of currently filled triangulation
+  NCollection_Vector<Graphic3d_Vec3> myNormals;   //!< normals of currently filled triangulation
+  NCollection_Vector<Graphic3d_Vec2> myNodesUV;   //!< UVs     of currently filled triangulation
+  NCollection_Vector<Poly_Triangle>  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
index 5bfd2a3..2296ef2 100644 (file)
@@ -1,2 +1,3 @@
 RWGltf
 RWMesh
+RWObj
index 3bcade2..5a0551d 100644 (file)
@@ -44,6 +44,8 @@
 #include <Quantity_NameOfColor.hxx>
 #include <RWGltf_CafReader.hxx>
 #include <RWStl.hxx>
+#include <RWObj.hxx>
+#include <RWObj_CafReader.hxx>
 #include <SelectMgr_SelectionManager.hxx>
 #include <Standard_ErrorHandler.hxx>
 #include <StdSelect_ViewerSelector3d.hxx>
@@ -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<TCollection_AsciiString>::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 );
index d930f98..9ed1a89 100644 (file)
@@ -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 (file)
index 0000000..a6de429
--- /dev/null
@@ -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 (file)
index 0000000..df717a0
--- /dev/null
@@ -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 (file)
index 0000000..bdbad5b
--- /dev/null
@@ -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 (file)
index 0000000..231ba45
--- /dev/null
@@ -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 (file)
index 0000000..206876b
--- /dev/null
@@ -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