0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file
authorkgv <kgv@opencascade.com>
Mon, 31 May 2021 17:14:03 +0000 (20:14 +0300)
committerbugmaster <bugmaster@opencascade.com>
Thu, 3 Jun 2021 15:28:45 +0000 (18:28 +0300)
Unstable test case v3d/memory/bug26538 has been adjusted.

21 files changed:
src/Message/FILES
src/Message/Message_LazyProgressScope.hxx [new file with mode: 0644]
src/RWGltf/RWGltf_CafWriter.cxx
src/RWMesh/RWMesh_FaceIterator.cxx
src/RWMesh/RWMesh_FaceIterator.hxx
src/RWObj/FILES
src/RWObj/RWObj_CafWriter.cxx [new file with mode: 0644]
src/RWObj/RWObj_CafWriter.hxx [new file with mode: 0644]
src/RWObj/RWObj_ObjMaterialMap.cxx [new file with mode: 0644]
src/RWObj/RWObj_ObjMaterialMap.hxx [new file with mode: 0644]
src/RWObj/RWObj_ObjWriterContext.cxx [new file with mode: 0644]
src/RWObj/RWObj_ObjWriterContext.hxx [new file with mode: 0644]
src/XCAFPrs/XCAFPrs_Style.hxx
src/XSDRAWSTLVRML/XSDRAWSTLVRML.cxx
tests/de_mesh/grids.list
tests/de_mesh/obj_write/as1 [new file with mode: 0644]
tests/de_mesh/obj_write/ball [new file with mode: 0644]
tests/de_mesh/obj_write/lantern [new file with mode: 0644]
tests/de_mesh/obj_write/mustang [new file with mode: 0644]
tests/de_mesh/obj_write/ship_boat [new file with mode: 0644]
tests/v3d/memory/bug26538

index 1f553d2..f611229 100755 (executable)
@@ -21,6 +21,7 @@ Message_CompositeAlerts.hxx
 Message_ExecStatus.hxx
 Message_Gravity.hxx
 Message_HArrayOfMsg.hxx
+Message_LazyProgressScope.hxx
 Message_Level.cxx
 Message_Level.hxx
 Message_ListIteratorOfListOfMsg.hxx
diff --git a/src/Message/Message_LazyProgressScope.hxx b/src/Message/Message_LazyProgressScope.hxx
new file mode 100644 (file)
index 0000000..a4ddef8
--- /dev/null
@@ -0,0 +1,80 @@
+// Copyright (c) 2017-2021 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 _Message_LazyProgressScope_HeaderFiler
+#define _Message_LazyProgressScope_HeaderFiler
+
+#include <Message_ProgressScope.hxx>
+
+//! Progress scope with lazy updates and abort fetches.
+//!
+//! Although Message_ProgressIndicator implementation is encouraged to spare GUI updates,
+//! even optimized implementation might show a noticeable overhead on a very small update step (e.g. per triangle).
+//!
+//! The class splits initial (displayed) number of overall steps into larger chunks specified in constructor,
+//! so that displayed progress is updated at larger steps.
+class Message_LazyProgressScope : protected Message_ProgressScope
+{
+public:
+
+  //! Main constructor.
+  //! @param theRange [in] progress range to scope
+  //! @param theName  [in] name of this scope
+  //! @param theMax   [in] number of steps within this scope
+  //! @param thePatchStep [in] number of steps to update progress
+  //! @param theIsInf [in] infinite flag
+  Message_LazyProgressScope (const Message_ProgressRange& theRange,
+                             const char* theName,
+                             const Standard_Real theMax,
+                             const Standard_Real thePatchStep,
+                             const Standard_Boolean theIsInf = Standard_False)
+  : Message_ProgressScope (theRange, theName, theMax, theIsInf),
+    myPatchStep (thePatchStep),
+    myPatchProgress (0.0),
+    myIsLazyAborted (Standard_False) {}
+
+  //! Increment progress with 1.
+  void Next()
+  {
+    if (++myPatchProgress < myPatchStep)
+    {
+      return;
+    }
+
+    myPatchProgress = 0.0;
+    Message_ProgressScope::Next (myPatchStep);
+    IsAborted();
+  }
+
+  //! Return TRUE if progress has been aborted - return the cached state lazily updated.
+  Standard_Boolean More() const
+  {
+    return !myIsLazyAborted;
+  }
+
+  //! Return TRUE if progress has been aborted - fetches actual value from the Progress.
+  Standard_Boolean IsAborted()
+  {
+    myIsLazyAborted = myIsLazyAborted || !Message_ProgressScope::More();
+    return myIsLazyAborted;
+  }
+
+protected:
+
+  Standard_Real    myPatchStep;
+  Standard_Real    myPatchProgress;
+  Standard_Boolean myIsLazyAborted;
+
+};
+
+#endif // _Message_LazyProgressScope_HeaderFiler
index aebe943..3dedb7f 100644 (file)
@@ -1068,7 +1068,7 @@ void RWGltf_CafWriter::writeAsset (const TColStd_IndexedDataMapOfStringString& t
   myWriter->Key    (RWGltf_GltfRootElementName (RWGltf_GltfRootElement_Asset));
   myWriter->StartObject();
   myWriter->Key    ("generator");
-  myWriter->String ("Open CASCADE Technology [www.opencascade.com]");
+  myWriter->String ("Open CASCADE Technology [dev.opencascade.org]");
   myWriter->Key    ("version");
   myWriter->String ("2.0"); // glTF format version
 
index ec3dd5e..cdcd524 100644 (file)
@@ -127,7 +127,7 @@ void RWMesh_FaceIterator::dispatchStyles (const TDF_Label&       theLabel,
 // function : normal
 // purpose  :
 // =======================================================================
-gp_Dir RWMesh_FaceIterator::normal (Standard_Integer theNode)
+gp_Dir RWMesh_FaceIterator::normal (Standard_Integer theNode) const
 {
   gp_Dir aNormal (gp::DZ());
   if (myPolyTriang->HasNormals())
index 6547c11..f70b378 100644 (file)
@@ -100,7 +100,7 @@ public:
   bool HasTexCoords() const { return !myPolyTriang.IsNull() && myPolyTriang->HasUVNodes(); }
 
   //! Return normal at specified node index with face transformation applied and face orientation applied.
-  gp_Dir NormalTransformed (Standard_Integer theNode)
+  gp_Dir NormalTransformed (Standard_Integer theNode) const
   {
     gp_Dir aNorm = normal (theNode);
     if (myTrsf.Form() != gp_Identity)
@@ -148,7 +148,7 @@ public:
   gp_Pnt node (const Standard_Integer theNode) const { return myPolyTriang->Node (theNode); }
 
   //! Return normal at specified node index without face transformation applied.
-  Standard_EXPORT gp_Dir normal (Standard_Integer theNode);
+  Standard_EXPORT gp_Dir normal (Standard_Integer theNode) const;
 
   //! Return triangle with specified index.
   Poly_Triangle triangle (Standard_Integer theElemIndex) const { return myPolyTriang->Triangle (theElemIndex); }
@@ -185,7 +185,7 @@ private:
   TopoDS_Face                     myFace;         //!< current face
   Handle(Poly_Triangulation)      myPolyTriang;   //!< triangulation of current face
   TopLoc_Location                 myFaceLocation; //!< current face location
-  BRepLProp_SLProps               mySLTool;       //!< auxiliary tool for fetching normals from surface
+  mutable BRepLProp_SLProps       mySLTool;       //!< auxiliary tool for fetching normals from surface
   BRepAdaptor_Surface             myFaceAdaptor;  //!< surface adaptor for fetching normals from surface
   Standard_Boolean                myHasNormals;   //!< flag indicating that current face has normals
   gp_Trsf                         myTrsf;         //!< current face transformation
index f6a988e..2c31775 100644 (file)
@@ -2,9 +2,15 @@ RWObj.cxx
 RWObj.hxx
 RWObj_CafReader.cxx
 RWObj_CafReader.hxx
+RWObj_CafWriter.cxx
+RWObj_CafWriter.hxx
 RWObj_Material.hxx
 RWObj_MtlReader.cxx
 RWObj_MtlReader.hxx
+RWObj_ObjMaterialMap.cxx
+RWObj_ObjMaterialMap.hxx
+RWObj_ObjWriterContext.cxx
+RWObj_ObjWriterContext.hxx
 RWObj_Reader.cxx
 RWObj_Reader.hxx
 RWObj_SubMesh.hxx
diff --git a/src/RWObj/RWObj_CafWriter.cxx b/src/RWObj/RWObj_CafWriter.cxx
new file mode 100644 (file)
index 0000000..979af88
--- /dev/null
@@ -0,0 +1,416 @@
+// Copyright (c) 2015-2021 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_CafWriter.hxx>
+
+#include <Message.hxx>
+#include <Message_LazyProgressScope.hxx>
+#include <OSD_OpenFile.hxx>
+#include <OSD_Path.hxx>
+#include <RWMesh_FaceIterator.hxx>
+#include <RWMesh_MaterialMap.hxx>
+#include <RWObj_ObjMaterialMap.hxx>
+#include <RWObj_ObjWriterContext.hxx>
+#include <Standard_CLocaleSentry.hxx>
+#include <TDocStd_Document.hxx>
+#include <TDataStd_Name.hxx>
+#include <XCAFDoc_DocumentTool.hxx>
+#include <XCAFDoc_ShapeTool.hxx>
+#include <XCAFPrs_DocumentExplorer.hxx>
+
+IMPLEMENT_STANDARD_RTTIEXT(RWObj_CafWriter, Standard_Transient)
+
+namespace
+{
+  //! Trivial cast.
+  inline Graphic3d_Vec3 objXyzToVec (const gp_XYZ& thePnt)
+  {
+    return Graphic3d_Vec3 ((float )thePnt.X(), (float )thePnt.Y(), (float )thePnt.Z());
+  }
+
+  //! Trivial cast.
+  inline Graphic3d_Vec2 objXyToVec (const gp_XY& thePnt)
+  {
+    return Graphic3d_Vec2 ((float )thePnt.X(), (float )thePnt.Y());
+  }
+
+  //! Read name attribute.
+  static TCollection_AsciiString readNameAttribute (const TDF_Label& theRefLabel)
+  {
+    Handle(TDataStd_Name) aNodeName;
+    if (!theRefLabel.FindAttribute (TDataStd_Name::GetID(), aNodeName))
+    {
+      return TCollection_AsciiString();
+    }
+    return TCollection_AsciiString (aNodeName->Get());
+  }
+}
+
+//================================================================
+// Function : Constructor
+// Purpose  :
+//================================================================
+RWObj_CafWriter::RWObj_CafWriter (const TCollection_AsciiString& theFile)
+: myFile (theFile)
+{
+  // OBJ file format doesn't define length units;
+  // Y-up coordinate system is most commonly used (but also undefined)
+  //myCSTrsf.SetOutputCoordinateSystem (RWMesh_CoordinateSystem_negZfwd_posYup);
+}
+
+//================================================================
+// Function : Destructor
+// Purpose  :
+//================================================================
+RWObj_CafWriter::~RWObj_CafWriter()
+{
+  //
+}
+
+//================================================================
+// Function : toSkipFaceMesh
+// Purpose  :
+//================================================================
+Standard_Boolean RWObj_CafWriter::toSkipFaceMesh (const RWMesh_FaceIterator& theFaceIter)
+{
+  return theFaceIter.IsEmptyMesh();
+}
+
+// =======================================================================
+// function : Perform
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::Perform (const Handle(TDocStd_Document)& theDocument,
+                               const TColStd_IndexedDataMapOfStringString& theFileInfo,
+                               const Message_ProgressRange& theProgress)
+{
+  TDF_LabelSequence aRoots;
+  Handle(XCAFDoc_ShapeTool) aShapeTool = XCAFDoc_DocumentTool::ShapeTool (theDocument->Main());
+  aShapeTool->GetFreeShapes (aRoots);
+  return Perform (theDocument, aRoots, NULL, theFileInfo, theProgress);
+}
+
+// =======================================================================
+// function : Perform
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::Perform (const Handle(TDocStd_Document)& theDocument,
+                               const TDF_LabelSequence& theRootLabels,
+                               const TColStd_MapOfAsciiString* theLabelFilter,
+                               const TColStd_IndexedDataMapOfStringString& theFileInfo,
+                               const Message_ProgressRange& theProgress)
+{
+  TCollection_AsciiString aFolder, aFileName, aFullFileNameBase, aShortFileNameBase, aFileExt;
+  OSD_Path::FolderAndFileFromPath (myFile, aFolder, aFileName);
+  OSD_Path::FileNameAndExtension (aFileName, aShortFileNameBase, aFileExt);
+
+  if (theRootLabels.IsEmpty()
+  || (theLabelFilter != NULL && theLabelFilter->IsEmpty()))
+  {
+    Message::SendFail ("Nothing to export into OBJ file");
+    return false;
+  }
+
+  Standard_Integer aNbNodesAll = 0, aNbElemsAll = 0;
+  Standard_Real aNbPEntities = 0; // steps for progress range
+  bool toCreateMatFile = false;
+  for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_OnlyLeafNodes);
+       aDocExplorer.More(); aDocExplorer.Next())
+  {
+    const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
+    if (theLabelFilter != NULL
+    && !theLabelFilter->Contains (aDocNode.Id))
+    {
+      continue;
+    }
+
+    for (RWMesh_FaceIterator aFaceIter (aDocNode.RefLabel, aDocNode.Location, true, aDocNode.Style); aFaceIter.More(); aFaceIter.Next())
+    {
+      if (toSkipFaceMesh (aFaceIter))
+      {
+        continue;
+      }
+
+      addFaceInfo (aFaceIter, aNbNodesAll, aNbElemsAll, aNbPEntities, toCreateMatFile);
+    }
+  }
+  if (aNbNodesAll == 0
+   || aNbElemsAll == 0)
+  {
+    Message::SendFail ("No mesh data to save");
+    return false;
+  }
+
+  TCollection_AsciiString aMatFileNameShort = aShortFileNameBase + ".mtl";
+  const TCollection_AsciiString aMatFileNameFull  = !aFolder.IsEmpty() ? aFolder + aMatFileNameShort : aMatFileNameShort;
+  if (!toCreateMatFile)
+  {
+    aMatFileNameShort.Clear();
+  }
+
+  Standard_CLocaleSentry  aLocaleSentry;
+  RWObj_ObjWriterContext  anObjFile(myFile);
+  RWObj_ObjMaterialMap    aMatMgr  (aMatFileNameFull);
+  aMatMgr.SetDefaultStyle (myDefaultStyle);
+  if (!anObjFile.IsOpened()
+   || !anObjFile.WriteHeader (aNbNodesAll, aNbElemsAll, aMatFileNameShort, theFileInfo))
+  {
+    return false;
+  }
+
+  int aRootDepth = 0;
+  if (theRootLabels.Size() == 1)
+  {
+    TDF_Label aRefLabel = theRootLabels.First();
+    XCAFDoc_ShapeTool::GetReferredShape (theRootLabels.First(), aRefLabel);
+    TCollection_AsciiString aRootName = readNameAttribute (aRefLabel);
+    if (aRootName.EndsWith (".obj"))
+    {
+      // workaround import/export of .obj file
+      aRootDepth = 1;
+    }
+  }
+
+  // simple global progress sentry - ignores size of node and index data
+  const Standard_Real aPatchStep = 2048.0; // about 100 KiB
+  Message_LazyProgressScope aPSentry (theProgress, "OBJ export", aNbPEntities, aPatchStep);
+
+  bool isDone = true;
+  for (XCAFPrs_DocumentExplorer aDocExplorer (theDocument, theRootLabels, XCAFPrs_DocumentExplorerFlags_OnlyLeafNodes);
+       aDocExplorer.More() && !aPSentry.IsAborted(); aDocExplorer.Next())
+  {
+    const XCAFPrs_DocumentNode& aDocNode = aDocExplorer.Current();
+    if (theLabelFilter != NULL
+    && !theLabelFilter->Contains (aDocNode.Id))
+    {
+      continue;
+    }
+
+    TCollection_AsciiString aName = readNameAttribute (aDocNode.RefLabel);
+    for (int aParentIter = aDocExplorer.CurrentDepth() - 1; aParentIter >= aRootDepth; --aParentIter)
+    {
+      const TCollection_AsciiString aParentName = readNameAttribute (aDocExplorer.Current (aParentIter).RefLabel);
+      if (!aParentName.IsEmpty())
+      {
+        aName = aParentName + "/" + aName;
+      }
+    }
+
+    if (!writeShape (anObjFile, aMatMgr, aPSentry, aDocNode.RefLabel, aDocNode.Location, aDocNode.Style, aName))
+    {
+      isDone = false;
+      break;
+    }
+  }
+
+  const bool isClosed = anObjFile.Close();
+  if (isDone && !isClosed)
+  {
+    Message::SendFail (TCollection_AsciiString ("Failed to write OBJ file\n") + myFile);
+    return false;
+  }
+  return isDone && !aPSentry.IsAborted();
+}
+
+// =======================================================================
+// function : addFaceInfo
+// purpose  :
+// =======================================================================
+void RWObj_CafWriter::addFaceInfo (const RWMesh_FaceIterator& theFace,
+                                   Standard_Integer& theNbNodes,
+                                   Standard_Integer& theNbElems,
+                                   Standard_Real& theNbProgressSteps,
+                                   Standard_Boolean& theToCreateMatFile)
+{
+  theNbNodes += theFace.NbNodes();
+  theNbElems += theFace.NbTriangles();
+
+  theNbProgressSteps += theFace.NbNodes();
+  theNbProgressSteps += theFace.NbTriangles();
+  if (theFace.HasNormals())
+  {
+    theNbProgressSteps += theFace.NbNodes();
+  }
+  if (theFace.HasTexCoords()) //&& !theFace.FaceStyle().Texture().IsEmpty()
+  {
+    theNbProgressSteps += theFace.NbNodes();
+  }
+
+  theToCreateMatFile = theToCreateMatFile
+                   ||  theFace.HasFaceColor()
+                   || (!theFace.FaceStyle().BaseColorTexture().IsNull() && theFace.HasTexCoords());
+}
+
+// =======================================================================
+// function : writeShape
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::writeShape (RWObj_ObjWriterContext&        theWriter,
+                                  RWObj_ObjMaterialMap&          theMatMgr,
+                                  Message_LazyProgressScope&     thePSentry,
+                                  const TDF_Label&               theLabel,
+                                  const TopLoc_Location&         theParentTrsf,
+                                  const XCAFPrs_Style&           theParentStyle,
+                                  const TCollection_AsciiString& theName)
+{
+  bool toCreateGroup = true;
+  for (RWMesh_FaceIterator aFaceIter (theLabel, theParentTrsf, true, theParentStyle); aFaceIter.More() && !thePSentry.IsAborted(); aFaceIter.Next())
+  {
+    if (toSkipFaceMesh (aFaceIter))
+    {
+      continue;
+    }
+
+    ++theWriter.NbFaces;
+    {
+      const bool hasNormals   = aFaceIter.HasNormals();
+      const bool hasTexCoords = aFaceIter.HasTexCoords(); //&& !aFaceIter.FaceStyle().Texture().IsEmpty();
+      if (theWriter.NbFaces != 1)
+      {
+        toCreateGroup = toCreateGroup
+                     || hasNormals   != theWriter.HasNormals()
+                     || hasTexCoords != theWriter.HasTexCoords();
+      }
+      theWriter.SetNormals  (hasNormals);
+      theWriter.SetTexCoords(hasTexCoords);
+    }
+
+    if (toCreateGroup
+    && !theWriter.WriteGroup (theName))
+    {
+      return false;
+    }
+    toCreateGroup = false;
+
+    TCollection_AsciiString aMatName;
+    if (aFaceIter.HasFaceColor()
+    || !aFaceIter.FaceStyle().BaseColorTexture().IsNull())
+    {
+      aMatName = theMatMgr.AddMaterial (aFaceIter.FaceStyle());
+    }
+    if (aMatName != theWriter.ActiveMaterial())
+    {
+      theWriter.WriteActiveMaterial (aMatName);
+    }
+
+    // write nodes
+    if (!writePositions (theWriter, thePSentry, aFaceIter))
+    {
+      return false;
+    }
+
+    // write normals
+    if (theWriter.HasNormals()
+    && !writeNormals (theWriter, thePSentry, aFaceIter))
+    {
+      return false;
+    }
+
+    if (theWriter.HasTexCoords()
+    && !writeTextCoords (theWriter, thePSentry, aFaceIter))
+    {
+      return false;
+    }
+
+    if (!writeIndices (theWriter, thePSentry, aFaceIter))
+    {
+      return false;
+    }
+    theWriter.FlushFace (aFaceIter.NbNodes());
+  }
+  return true;
+}
+
+// =======================================================================
+// function : writePositions
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::writePositions (RWObj_ObjWriterContext&    theWriter,
+                                      Message_LazyProgressScope& thePSentry,
+                                      const RWMesh_FaceIterator& theFace)
+{
+  const Standard_Integer aNodeUpper = theFace.NodeUpper();
+  for (Standard_Integer aNodeIter = theFace.NodeLower(); aNodeIter <= aNodeUpper && thePSentry.More(); ++aNodeIter, thePSentry.Next())
+  {
+    gp_XYZ aNode = theFace.NodeTransformed (aNodeIter).XYZ();
+    myCSTrsf.TransformPosition (aNode);
+    if (!theWriter.WriteVertex (objXyzToVec (aNode)))
+    {
+      return false;
+    }
+  }
+  return true;
+}
+
+// =======================================================================
+// function : writeNormals
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::writeNormals (RWObj_ObjWriterContext&    theWriter,
+                                    Message_LazyProgressScope& thePSentry,
+                                    const RWMesh_FaceIterator& theFace)
+{
+  const Standard_Integer aNodeUpper = theFace.NodeUpper();
+  for (Standard_Integer aNodeIter = theFace.NodeLower(); aNodeIter <= aNodeUpper && thePSentry.More(); ++aNodeIter, thePSentry.Next())
+  {
+    const gp_Dir aNormal = theFace.NormalTransformed (aNodeIter);
+    Graphic3d_Vec3 aNormVec3 = objXyzToVec (aNormal.XYZ());
+    myCSTrsf.TransformNormal (aNormVec3);
+    if (!theWriter.WriteNormal (aNormVec3))
+    {
+      return false;
+    }
+  }
+  return true;
+}
+
+// =======================================================================
+// function : writeTextCoords
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::writeTextCoords (RWObj_ObjWriterContext&    theWriter,
+                                       Message_LazyProgressScope& thePSentry,
+                                       const RWMesh_FaceIterator& theFace)
+{
+  const Standard_Integer aNodeUpper = theFace.NodeUpper();
+  for (Standard_Integer aNodeIter = theFace.NodeLower(); aNodeIter <= aNodeUpper && thePSentry.More(); ++aNodeIter, thePSentry.Next())
+  {
+    gp_Pnt2d aTexCoord = theFace.NodeTexCoord (aNodeIter);
+    if (!theWriter.WriteTexCoord (objXyToVec (aTexCoord.XY())))
+    {
+      return false;
+    }
+  }
+  return true;
+}
+
+// =======================================================================
+// function : writeIndices
+// purpose  :
+// =======================================================================
+bool RWObj_CafWriter::writeIndices (RWObj_ObjWriterContext&    theWriter,
+                                    Message_LazyProgressScope& thePSentry,
+                                    const RWMesh_FaceIterator& theFace)
+{
+  const Standard_Integer anElemLower = theFace.ElemLower();
+  const Standard_Integer anElemUpper = theFace.ElemUpper();
+  for (Standard_Integer anElemIter = anElemLower; anElemIter <= anElemUpper && thePSentry.More(); ++anElemIter, thePSentry.Next())
+  {
+    const Poly_Triangle aTri = theFace.TriangleOriented (anElemIter);
+    if (!theWriter.WriteTriangle (Graphic3d_Vec3i (aTri(1), aTri(2), aTri(3)) - Graphic3d_Vec3i (anElemLower)))
+    {
+      return false;
+    }
+  }
+  return true;
+}
diff --git a/src/RWObj/RWObj_CafWriter.hxx b/src/RWObj/RWObj_CafWriter.hxx
new file mode 100644 (file)
index 0000000..001f38b
--- /dev/null
@@ -0,0 +1,167 @@
+// Copyright (c) 2015-2021 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_CafWriter_HeaderFiler
+#define _RWObj_CafWriter_HeaderFiler
+
+#include <TColStd_IndexedDataMapOfStringString.hxx>
+#include <TColStd_MapOfAsciiString.hxx>
+#include <TDF_LabelSequence.hxx>
+#include <TopTools_ShapeMapHasher.hxx>
+#include <RWMesh_CoordinateSystemConverter.hxx>
+#include <XCAFPrs_Style.hxx>
+
+#include <memory>
+
+class Message_ProgressRange;
+class RWMesh_FaceIterator;
+class TDocStd_Document;
+
+class Message_LazyProgressScope;
+class RWObj_ObjWriterContext;
+class RWObj_ObjMaterialMap;
+
+//! OBJ writer context from XCAF document.
+class RWObj_CafWriter : public Standard_Transient
+{
+  DEFINE_STANDARD_RTTIEXT(RWObj_CafWriter, Standard_Transient)
+public:
+
+  //! Main constructor.
+  //! @param theFile [in] path to output OBJ file
+  Standard_EXPORT RWObj_CafWriter (const TCollection_AsciiString& theFile);
+
+  //! Destructor.
+  Standard_EXPORT virtual ~RWObj_CafWriter();
+
+  //! Return transformation from OCCT to OBJ coordinate system.
+  const RWMesh_CoordinateSystemConverter& CoordinateSystemConverter() const { return myCSTrsf; }
+
+  //! Return transformation from OCCT to OBJ coordinate system.
+  RWMesh_CoordinateSystemConverter& ChangeCoordinateSystemConverter() { return myCSTrsf; }
+
+  //! Set transformation from OCCT to OBJ coordinate system.
+  void SetCoordinateSystemConverter (const RWMesh_CoordinateSystemConverter& theConverter) { myCSTrsf = theConverter; }
+
+  //! Return default material definition to be used for nodes with only color defined.
+  const XCAFPrs_Style& DefaultStyle() const { return myDefaultStyle; }
+
+  //! Set default material definition to be used for nodes with only color defined.
+  void SetDefaultStyle (const XCAFPrs_Style& theStyle) { myDefaultStyle = theStyle; }
+
+  //! Write OBJ file and associated MTL material file.
+  //! Triangulation data should be precomputed within shapes!
+  //! @param theDocument    [in] input document
+  //! @param theRootLabels  [in] list of root shapes to export
+  //! @param theLabelFilter [in] optional filter with document nodes to export,
+  //!                            with keys defined by XCAFPrs_DocumentExplorer::DefineChildId() and filled recursively
+  //!                            (leaves and parent assembly nodes at all levels);
+  //!                            when not NULL, all nodes not included into the map will be ignored
+  //! @param theFileInfo    [in] map with file metadata to put into OBJ header section
+  //! @param theProgress    [in] optional progress indicator
+  //! @return FALSE on file writing failure
+  Standard_EXPORT virtual bool Perform (const Handle(TDocStd_Document)& theDocument,
+                                        const TDF_LabelSequence& theRootLabels,
+                                        const TColStd_MapOfAsciiString* theLabelFilter,
+                                        const TColStd_IndexedDataMapOfStringString& theFileInfo,
+                                        const Message_ProgressRange& theProgress);
+
+  //! Write OBJ file and associated MTL material file.
+  //! Triangulation data should be precomputed within shapes!
+  //! @param theDocument    [in] input document
+  //! @param theFileInfo    [in] map with file metadata to put into glTF header section
+  //! @param theProgress    [in] optional progress indicator
+  //! @return FALSE on file writing failure
+  Standard_EXPORT virtual bool Perform (const Handle(TDocStd_Document)& theDocument,
+                                        const TColStd_IndexedDataMapOfStringString& theFileInfo,
+                                        const Message_ProgressRange& theProgress);
+
+protected:
+
+  //! Return TRUE if face mesh should be skipped (e.g. because it is invalid or empty).
+  Standard_EXPORT virtual Standard_Boolean toSkipFaceMesh (const RWMesh_FaceIterator& theFaceIter);
+
+  //! Collect face triangulation info.
+  //! @param theFace [in] face to process
+  //! @param theNbNodes [in] [out] overall number of triangulation nodes (should be appended)
+  //! @param theNbElems [in] [out] overall number of triangulation elements (should be appended)
+  //! @param theNbProgressSteps [in] [out] overall number of progress steps (should be appended)
+  //! @param theToCreateMatFile [in] [out] flag to create material file or not (should be appended)
+  Standard_EXPORT virtual void addFaceInfo (const RWMesh_FaceIterator& theFace,
+                                            Standard_Integer& theNbNodes,
+                                            Standard_Integer& theNbElems,
+                                            Standard_Real& theNbProgressSteps,
+                                            Standard_Boolean& theToCreateMatFile);
+
+  //! Write the shape.
+  //! @param theWriter  [in] OBJ writer context
+  //! @param theMatMgr  [in] OBJ material map
+  //! @param thePSentry [in] progress sentry
+  //! @param theLabel   [in] document label to process
+  //! @param theParentTrsf  [in] parent node transformation
+  //! @param theParentStyle [in] parent node style
+  //! @param theName    [in] node name
+  Standard_EXPORT virtual bool writeShape (RWObj_ObjWriterContext&        theWriter,
+                                           RWObj_ObjMaterialMap&          theMatMgr,
+                                           Message_LazyProgressScope&     thePSentry,
+                                           const TDF_Label&               theLabel,
+                                           const TopLoc_Location&         theParentTrsf,
+                                           const XCAFPrs_Style&           theParentStyle,
+                                           const TCollection_AsciiString& theName);
+
+  //! Write face triangle vertex positions.
+  //! @param theWriter  [in] OBJ writer context
+  //! @param thePSentry [in] progress sentry
+  //! @param theFace    [in] current face
+  //! @return FALSE on writing file error
+  Standard_EXPORT virtual bool writePositions (RWObj_ObjWriterContext&    theWriter,
+                                               Message_LazyProgressScope& thePSentry,
+                                               const RWMesh_FaceIterator& theFace);
+
+  //! Write face triangle vertex normals.
+  //! @param theWriter  [in] OBJ writer context
+  //! @param thePSentry [in] progress sentry
+  //! @param theFace    [in] current face
+  //! @return FALSE on writing file error
+  Standard_EXPORT virtual bool writeNormals (RWObj_ObjWriterContext&    theWriter,
+                                             Message_LazyProgressScope& thePSentry,
+                                             const RWMesh_FaceIterator& theFace);
+
+  //! Write face triangle vertex texture coordinates.
+  //! @param theWriter  [in] OBJ writer context
+  //! @param thePSentry [in] progress sentry
+  //! @param theFace    [in] current face
+  //! @return FALSE on writing file error
+  Standard_EXPORT virtual bool writeTextCoords (RWObj_ObjWriterContext&    theWriter,
+                                                Message_LazyProgressScope& thePSentry,
+                                                const RWMesh_FaceIterator& theFace);
+
+  //! Write face triangles indices.
+  //! @param theWriter  [in] OBJ writer context
+  //! @param thePSentry [in] progress sentry
+  //! @param theFace    [in] current face
+  //! @return FALSE on writing file error
+  Standard_EXPORT virtual bool writeIndices (RWObj_ObjWriterContext&    theWriter,
+                                             Message_LazyProgressScope& thePSentry,
+                                             const RWMesh_FaceIterator& theFace);
+
+
+protected:
+
+  TCollection_AsciiString          myFile;         //!< output OBJ file
+  RWMesh_CoordinateSystemConverter myCSTrsf;       //!< transformation from OCCT to OBJ coordinate system
+  XCAFPrs_Style                    myDefaultStyle; //!< default material definition to be used for nodes with only color defined
+
+};
+
+#endif // _RWObj_CafWriter_HeaderFiler
diff --git a/src/RWObj/RWObj_ObjMaterialMap.cxx b/src/RWObj/RWObj_ObjMaterialMap.cxx
new file mode 100644 (file)
index 0000000..7007f29
--- /dev/null
@@ -0,0 +1,153 @@
+// Copyright (c) 2015-2021 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_ObjMaterialMap.hxx>
+
+#include <Message.hxx>
+#include <OSD_OpenFile.hxx>
+
+IMPLEMENT_STANDARD_RTTIEXT(RWObj_ObjMaterialMap, RWMesh_MaterialMap)
+
+// ================================================================
+// Function : RWObj_ObjMaterialMap
+// Purpose  :
+// ================================================================
+RWObj_ObjMaterialMap::RWObj_ObjMaterialMap (const TCollection_AsciiString& theFile)
+: RWMesh_MaterialMap (theFile),
+  myFile (NULL)
+{
+  //
+}
+
+// ================================================================
+// Function : ~RWObj_ObjMaterialMap
+// Purpose  :
+// ================================================================
+RWObj_ObjMaterialMap::~RWObj_ObjMaterialMap()
+{
+  if (myFile != NULL)
+  {
+    if (::fclose (myFile) != 0)
+    {
+      myIsFailed = true;
+    }
+  }
+
+  if (myIsFailed)
+  {
+    Message::SendFail (TCollection_AsciiString ("File cannot be written\n") + myFileName);
+  }
+}
+
+// ================================================================
+// Function : AddMaterial
+// Purpose  :
+// ================================================================
+TCollection_AsciiString RWObj_ObjMaterialMap::AddMaterial (const XCAFPrs_Style& theStyle)
+{
+  if (myFile == NULL
+  && !myIsFailed)
+  {
+    myFile = OSD_OpenFile (myFileName.ToCString(), "wb");
+    myIsFailed = myFile == NULL;
+    if (myFile != NULL)
+    {
+      Fprintf (myFile, "# Exported by Open CASCADE Technology [dev.opencascade.org]\n");
+    }
+  }
+  if (myFile == NULL)
+  {
+    return TCollection_AsciiString();
+  }
+
+  return RWMesh_MaterialMap::AddMaterial (theStyle);
+}
+
+// ================================================================
+// Function : DefineMaterial
+// Purpose  :
+// ================================================================
+void RWObj_ObjMaterialMap::DefineMaterial (const XCAFPrs_Style& theStyle,
+                                           const TCollection_AsciiString& theKey,
+                                           const TCollection_AsciiString& theName)
+{
+  (void )theName;
+  Fprintf (myFile, "newmtl %s\n", theKey.ToCString());
+
+  bool hasMaterial = false;
+  const XCAFDoc_VisMaterialCommon aDefMat = !myDefaultStyle.Material().IsNull()
+                                           ? myDefaultStyle.Material()->ConvertToCommonMaterial()
+                                           : XCAFDoc_VisMaterialCommon();
+  Quantity_Color anAmbQ (aDefMat.AmbientColor), aDiffQ (aDefMat.DiffuseColor), aSpecQ (aDefMat.SpecularColor);
+  Standard_ShortReal aTransp = 0.0f;
+  Standard_ShortReal aSpecular = aDefMat.Shininess * 1000.0f;
+  if (!theStyle.Material().IsNull()
+   && !theStyle.Material()->IsEmpty())
+  {
+    hasMaterial = true;
+    const XCAFDoc_VisMaterialCommon aComMat = theStyle.Material()->ConvertToCommonMaterial();
+    anAmbQ  = aComMat.AmbientColor;
+    aDiffQ  = aComMat.DiffuseColor;
+    aSpecQ  = aComMat.SpecularColor;
+    aTransp = aComMat.Transparency;
+    aSpecular = aComMat.Shininess * 1000.0f;
+  }
+  if (theStyle.IsSetColorSurf())
+  {
+    hasMaterial = true;
+    aDiffQ = theStyle.GetColorSurf();
+    anAmbQ = Quantity_Color ((Graphic3d_Vec3 )theStyle.GetColorSurf() * 0.25f);
+    if (theStyle.GetColorSurfRGBA().Alpha() < 1.0f)
+    {
+      aTransp = 1.0f - theStyle.GetColorSurfRGBA().Alpha();
+    }
+  }
+
+  if (hasMaterial)
+  {
+    Graphic3d_Vec3d anAmb, aDiff, aSpec;
+    anAmbQ.Values (anAmb.r(), anAmb.g(), anAmb.b(), Quantity_TOC_sRGB);
+    aDiffQ.Values (aDiff.r(), aDiff.g(), aDiff.b(), Quantity_TOC_sRGB);
+    aSpecQ.Values (aSpec.r(), aSpec.g(), aSpec.b(), Quantity_TOC_sRGB);
+
+    Fprintf (myFile, "Ka %f %f %f\n", anAmb.r(), anAmb.g(), anAmb.b());
+    Fprintf (myFile, "Kd %f %f %f\n", aDiff.r(), aDiff.g(), aDiff.b());
+    Fprintf (myFile, "Ks %f %f %f\n", aSpec.r(), aSpec.g(), aSpec.b());
+    Fprintf (myFile, "Ns %f\n", aSpecular);
+    if (aTransp >= 0.0001f)
+    {
+      Fprintf (myFile, "Tr %f\n", aTransp);
+    }
+  }
+
+  if (const Handle(Image_Texture)& aBaseTexture = theStyle.BaseColorTexture())
+  {
+    TCollection_AsciiString aTexture;
+    if (!myImageMap.Find (aBaseTexture, aTexture)
+     && !myImageFailMap.Contains (aBaseTexture))
+    {
+      if (CopyTexture (aTexture, aBaseTexture, TCollection_AsciiString (myImageMap.Extent() + 1)))
+      {
+        myImageMap.Bind (aBaseTexture, aTexture);
+      }
+      else
+      {
+        myImageFailMap.Add (aBaseTexture);
+      }
+    }
+    if (!aTexture.IsEmpty())
+    {
+      Fprintf (myFile, "map_Kd %s\n", aTexture.ToCString());
+    }
+  }
+}
diff --git a/src/RWObj/RWObj_ObjMaterialMap.hxx b/src/RWObj/RWObj_ObjMaterialMap.hxx
new file mode 100644 (file)
index 0000000..fd9df2e
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (c) 2015-2021 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_ObjMaterialMap_HeaderFiler
+#define _RWObj_ObjMaterialMap_HeaderFiler
+
+#include <RWMesh_MaterialMap.hxx>
+
+//! Material MTL file writer for OBJ export.
+class RWObj_ObjMaterialMap : public RWMesh_MaterialMap
+{
+  DEFINE_STANDARD_RTTIEXT(RWObj_ObjMaterialMap, RWMesh_MaterialMap)
+public:
+
+  //! Main constructor.
+  Standard_EXPORT RWObj_ObjMaterialMap (const TCollection_AsciiString& theFile);
+
+  //! Destructor, will emit error message if file was not closed.
+  Standard_EXPORT virtual ~RWObj_ObjMaterialMap();
+
+  //! Add material
+  Standard_EXPORT virtual TCollection_AsciiString AddMaterial (const XCAFPrs_Style& theStyle) Standard_OVERRIDE;
+
+  //! Virtual method actually defining the material (e.g. export to the file).
+  Standard_EXPORT virtual void DefineMaterial (const XCAFPrs_Style& theStyle,
+                                               const TCollection_AsciiString& theKey,
+                                               const TCollection_AsciiString& theName) Standard_OVERRIDE;
+
+private:
+
+  FILE* myFile;
+  NCollection_DataMap<Handle(Image_Texture), TCollection_AsciiString, Image_Texture> myImageMap;
+
+};
+
+#endif // _RWObj_ObjMaterialMap_HeaderFiler
diff --git a/src/RWObj/RWObj_ObjWriterContext.cxx b/src/RWObj/RWObj_ObjWriterContext.cxx
new file mode 100644 (file)
index 0000000..e7ce1e6
--- /dev/null
@@ -0,0 +1,305 @@
+// Copyright (c) 2015-2021 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_ObjWriterContext.hxx>
+
+#include <Message.hxx>
+#include <NCollection_IndexedMap.hxx>
+#include <OSD_OpenFile.hxx>
+
+// =======================================================================
+// function : splitLines
+// purpose  :
+// =======================================================================
+static void splitLines (const TCollection_AsciiString& theString,
+                        NCollection_IndexedMap<TCollection_AsciiString>& theLines)
+{
+  if (theString.IsEmpty())
+  {
+    return;
+  }
+
+  Standard_Integer aLineFrom = 1;
+  for (Standard_Integer aCharIter = 1;; ++aCharIter)
+  {
+    const char aChar = theString.Value (aCharIter);
+    if (aChar != '\r'
+        && aChar != '\n'
+        && aCharIter != theString.Length())
+    {
+      continue;
+    }
+
+    if (aLineFrom != aCharIter)
+    {
+      TCollection_AsciiString aLine = theString.SubString (aLineFrom, aCharIter);
+      aLine.RightAdjust();
+      theLines.Add (aLine);
+    }
+
+    if (aCharIter == theString.Length())
+    {
+      break;
+    }
+    else if (aChar == '\r'
+             && theString.Value (aCharIter + 1) == '\n')
+    {
+      // CRLF
+      ++aCharIter;
+    }
+    aLineFrom = aCharIter + 1;
+  }
+}
+
+// ================================================================
+// Function : RWObj_ObjWriterContext
+// Purpose  :
+// ================================================================
+RWObj_ObjWriterContext::RWObj_ObjWriterContext (const TCollection_AsciiString& theName)
+: NbFaces (0),
+  myFile (OSD_OpenFile (theName.ToCString(), "wb")),
+  myName (theName),
+  myElemPosFirst (1, 1, 1, 1),
+  myElemNormFirst(1, 1, 1, 1),
+  myElemUVFirst  (1, 1, 1, 1),
+  myHasNormals   (false),
+  myHasTexCoords (false)
+{
+  if (myFile == NULL)
+  {
+    Message::SendFail (TCollection_AsciiString ("File cannot be created\n") + theName);
+    return;
+  }
+}
+
+// ================================================================
+// Function : ~RWObj_ObjWriterContext
+// Purpose  :
+// ================================================================
+RWObj_ObjWriterContext::~RWObj_ObjWriterContext()
+{
+  if (myFile != NULL)
+  {
+    ::fclose (myFile);
+    Message::SendFail (TCollection_AsciiString ("File cannot be written\n") + myName);
+  }
+}
+
+// ================================================================
+// Function : Close
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::Close()
+{
+  bool isOk = ::fclose (myFile) == 0;
+  myFile = NULL;
+  return isOk;
+}
+
+// ================================================================
+// Function : WriteHeader
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteHeader (const Standard_Integer theNbNodes,
+                                          const Standard_Integer theNbElems,
+                                          const TCollection_AsciiString& theMatLib,
+                                          const TColStd_IndexedDataMapOfStringString& theFileInfo)
+{
+  bool isOk = ::Fprintf (myFile, "# Exported by Open CASCADE Technology [dev.opencascade.org]\n"
+                                 "#  Vertices: %d\n"
+                                 "#     Faces: %d\n", theNbNodes, theNbElems) != 0;
+  for (TColStd_IndexedDataMapOfStringString::Iterator aKeyValueIter (theFileInfo); aKeyValueIter.More(); aKeyValueIter.Next())
+  {
+    NCollection_IndexedMap<TCollection_AsciiString> aKeyLines, aValLines;
+    splitLines (aKeyValueIter.Key(),   aKeyLines);
+    splitLines (aKeyValueIter.Value(), aValLines);
+    for (Standard_Integer aLineIter = 1; aLineIter <= aKeyLines.Extent(); ++aLineIter)
+    {
+      const TCollection_AsciiString& aLine = aKeyLines.FindKey (aLineIter);
+      isOk = isOk
+        && ::Fprintf (myFile,
+                      aLineIter > 1 ? "\n# %s" : "# %s",
+                      aLine.ToCString()) != 0;
+    }
+    isOk = isOk
+      && ::Fprintf (myFile, !aKeyLines.IsEmpty() ? ":" : "# ") != 0;
+    for (Standard_Integer aLineIter = 1; aLineIter <= aValLines.Extent(); ++aLineIter)
+    {
+      const TCollection_AsciiString& aLine = aValLines.FindKey (aLineIter);
+      isOk = isOk
+        && ::Fprintf (myFile,
+                      aLineIter > 1 ? "\n# %s" : " %s",
+                      aLine.ToCString()) != 0;
+    }
+    isOk = isOk
+      && ::Fprintf (myFile, "\n") != 0;
+  }
+
+  if (!theMatLib.IsEmpty())
+  {
+    isOk = isOk
+        && ::Fprintf (myFile, "mtllib %s\n", theMatLib.ToCString()) != 0;
+  }
+  return isOk;
+}
+
+// ================================================================
+// Function : WriteActiveMaterial
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteActiveMaterial (const TCollection_AsciiString& theMaterial)
+{
+  myActiveMaterial = theMaterial;
+  return !theMaterial.IsEmpty()
+        ? Fprintf (myFile, "usemtl %s\n", theMaterial.ToCString()) != 0
+        : Fprintf (myFile, "usemtl\n") != 0;
+}
+
+// ================================================================
+// Function : WriteTriangle
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteTriangle (const Graphic3d_Vec3i& theTri)
+{
+  const Graphic3d_Vec3i aTriPos = theTri + myElemPosFirst.xyz();
+  if (myHasNormals)
+  {
+    const Graphic3d_Vec3i aTriNorm = theTri + myElemNormFirst.xyz();
+    if (myHasTexCoords)
+    {
+      const Graphic3d_Vec3i aTriUv = theTri + myElemUVFirst.xyz();
+      return Fprintf (myFile, "f %d/%d/%d %d/%d/%d %d/%d/%d\n",
+                      aTriPos[0], aTriUv[0], aTriNorm[0],
+                      aTriPos[1], aTriUv[1], aTriNorm[1],
+                      aTriPos[2], aTriUv[2], aTriNorm[2]) != 0;
+    }
+    else
+    {
+      return Fprintf (myFile, "f %d//%d %d//%d %d//%d\n",
+                      aTriPos[0], aTriNorm[0],
+                      aTriPos[1], aTriNorm[1],
+                      aTriPos[2], aTriNorm[2]) != 0;
+    }
+  }
+  if (myHasTexCoords)
+  {
+    const Graphic3d_Vec3i aTriUv = theTri + myElemUVFirst.xyz();
+    return Fprintf (myFile, "f %d/%d %d/%d %d/%d\n",
+                    aTriPos[0], aTriUv[0],
+                    aTriPos[1], aTriUv[1],
+                    aTriPos[2], aTriUv[2]) != 0;
+  }
+  else
+  {
+    return Fprintf (myFile, "f %d %d %d\n", aTriPos[0], aTriPos[1], aTriPos[2]) != 0;
+  }
+}
+
+// ================================================================
+// Function : WriteQuad
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteQuad (const Graphic3d_Vec4i& theQuad)
+{
+  const Graphic3d_Vec4i aQPos = theQuad + myElemPosFirst;
+  if (myHasNormals)
+  {
+    const Graphic3d_Vec4i aQNorm = theQuad + myElemNormFirst;
+    if (myHasTexCoords)
+    {
+      const Graphic3d_Vec4i aQTex = theQuad + myElemUVFirst;
+      return Fprintf (myFile, "f %d/%d/%d %d/%d/%d %d/%d/%d %d/%d/%d\n",
+                      aQPos[0], aQTex[0], aQNorm[0],
+                      aQPos[1], aQTex[1], aQNorm[1],
+                      aQPos[2], aQTex[2], aQNorm[2],
+                      aQPos[3], aQTex[3], aQNorm[3]) != 0;
+    }
+    else
+    {
+      return Fprintf (myFile, "f %d//%d %d//%d %d//%d %d//%d\n",
+                      aQPos[0], aQNorm[0],
+                      aQPos[1], aQNorm[1],
+                      aQPos[2], aQNorm[2],
+                      aQPos[3], aQNorm[3]) != 0;
+    }
+  }
+  if (myHasTexCoords)
+  {
+    const Graphic3d_Vec4i aQTex = theQuad + myElemUVFirst;
+    return Fprintf (myFile, "f %d/%d %d/%d %d/%d %d/%d\n",
+                    aQPos[0], aQTex[0],
+                    aQPos[1], aQTex[1],
+                    aQPos[2], aQTex[2],
+                    aQPos[3], aQTex[3]) != 0;
+  }
+  else
+  {
+    return Fprintf (myFile, "f %d %d %d %d\n", aQPos[0], aQPos[1], aQPos[2], aQPos[3]) != 0;
+  }
+}
+
+// ================================================================
+// Function : WriteVertex
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteVertex (const Graphic3d_Vec3& theValue)
+{
+  return Fprintf (myFile, "v %f %f %f\n",  theValue.x(), theValue.y(), theValue.z()) != 0;
+}
+
+// ================================================================
+// Function : WriteNormal
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteNormal (const Graphic3d_Vec3& theValue)
+{
+  return Fprintf (myFile, "vn %f %f %f\n", theValue.x(), theValue.y(), theValue.z()) != 0;
+}
+
+// ================================================================
+// Function : WriteTexCoord
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteTexCoord (const Graphic3d_Vec2& theValue)
+{
+  return Fprintf (myFile, "vt %f %f\n", theValue.x(), theValue.y()) != 0;
+}
+
+// ================================================================
+// Function : WriteGroup
+// Purpose  :
+// ================================================================
+bool RWObj_ObjWriterContext::WriteGroup (const TCollection_AsciiString& theValue)
+{
+  return !theValue.IsEmpty()
+        ? Fprintf (myFile, "g %s\n", theValue.ToCString()) != 0
+        : Fprintf (myFile, "g\n") != 0;
+}
+
+// ================================================================
+// Function : FlushFace
+// Purpose  :
+// ================================================================
+void RWObj_ObjWriterContext::FlushFace (Standard_Integer theNbNodes)
+{
+  Graphic3d_Vec4i aShift (theNbNodes, theNbNodes, theNbNodes, theNbNodes);
+  myElemPosFirst += aShift;
+  if (myHasNormals)
+  {
+    myElemNormFirst += aShift;
+  }
+  if (myHasTexCoords)
+  {
+    myElemUVFirst += aShift;
+  }
+}
diff --git a/src/RWObj/RWObj_ObjWriterContext.hxx b/src/RWObj/RWObj_ObjWriterContext.hxx
new file mode 100644 (file)
index 0000000..d5bff55
--- /dev/null
@@ -0,0 +1,100 @@
+// Copyright (c) 2015-2021 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_ObjWriterContext_HeaderFiler
+#define _RWObj_ObjWriterContext_HeaderFiler
+
+#include <Graphic3d_Vec.hxx>
+#include <TCollection_AsciiString.hxx>
+#include <TColStd_IndexedDataMapOfStringString.hxx>
+
+//! Auxiliary low-level tool writing OBJ file.
+class RWObj_ObjWriterContext
+{
+public:
+
+  //! Main constructor.
+  Standard_EXPORT RWObj_ObjWriterContext (const TCollection_AsciiString& theName);
+
+  //! Destructor, will emit error message if file was not closed.
+  Standard_EXPORT ~RWObj_ObjWriterContext();
+
+  //! Return true if file has been opened.
+  bool IsOpened() const { return myFile != NULL; }
+
+  //! Correctly close the file.
+  Standard_EXPORT bool Close();
+
+  //! Return true if normals are defined.
+  bool HasNormals() const { return myHasNormals; }
+
+  //! Set if normals are defined.
+  void SetNormals (const bool theHasNormals) { myHasNormals = theHasNormals; }
+
+  //! Return true if normals are defined.
+  bool HasTexCoords() const { return myHasTexCoords; }
+
+  //! Set if normals are defined.
+  void SetTexCoords (const bool theHasTexCoords) { myHasTexCoords = theHasTexCoords; }
+
+  //! Write the header.
+  Standard_EXPORT bool WriteHeader (const Standard_Integer theNbNodes,
+                                    const Standard_Integer theNbElems,
+                                    const TCollection_AsciiString& theMatLib,
+                                    const TColStd_IndexedDataMapOfStringString& theFileInfo);
+
+  //! Return active material or empty string if not set.
+  const TCollection_AsciiString& ActiveMaterial() const { return myActiveMaterial; }
+
+  //! Set active material.
+  Standard_EXPORT bool WriteActiveMaterial (const TCollection_AsciiString& theMaterial);
+
+  //! Writing a triangle
+  Standard_EXPORT bool WriteTriangle (const Graphic3d_Vec3i& theTri);
+
+  //! Writing a quad
+  Standard_EXPORT bool WriteQuad (const Graphic3d_Vec4i& theQuad);
+
+  //! Writing a vector
+  Standard_EXPORT bool WriteVertex (const Graphic3d_Vec3& theValue);
+
+  //! Writing a vector
+  Standard_EXPORT bool WriteNormal (const Graphic3d_Vec3& theValue);
+
+  //! Writing a vector
+  Standard_EXPORT bool WriteTexCoord (const Graphic3d_Vec2& theValue);
+
+  //! Writing a group name
+  Standard_EXPORT bool WriteGroup (const TCollection_AsciiString& theValue);
+
+  //! Increment indices shift.
+  Standard_EXPORT void FlushFace (Standard_Integer theNbNodes);
+
+public:
+
+  Standard_Integer NbFaces;
+
+private:
+
+  FILE* myFile;
+  TCollection_AsciiString myName;
+  TCollection_AsciiString myActiveMaterial;
+  Graphic3d_Vec4i myElemPosFirst;
+  Graphic3d_Vec4i myElemNormFirst;
+  Graphic3d_Vec4i myElemUVFirst;
+  bool myHasNormals;
+  bool myHasTexCoords;
+
+};
+
+#endif // _RWObj_ObjWriterContext_HeaderFiler
index cef8024..22c9664 100644 (file)
@@ -83,6 +83,27 @@ public:
   //! Manage visibility.
   Standard_Boolean IsVisible() const { return myIsVisible; }
 
+  //! Return base color texture.
+  const Handle(Image_Texture)& BaseColorTexture() const
+  {
+    static const Handle(Image_Texture) THE_NULL_TEXTURE;
+    if (myMaterial.IsNull())
+    {
+      return THE_NULL_TEXTURE;
+    }
+    else if (myMaterial->HasPbrMaterial()
+         && !myMaterial->PbrMaterial().BaseColorTexture.IsNull())
+    {
+      return myMaterial->PbrMaterial().BaseColorTexture;
+    }
+    else if (myMaterial->HasCommonMaterial()
+         && !myMaterial->CommonMaterial().DiffuseTexture.IsNull())
+    {
+      return myMaterial->CommonMaterial().DiffuseTexture;
+    }
+    return THE_NULL_TEXTURE;
+  }
+
   //! Returns True if styles are the same
   //! Methods for using Style as key in maps
   Standard_Boolean IsEqual (const XCAFPrs_Style& theOther) const
index 9a33cb7..4569f87 100644 (file)
@@ -47,6 +47,7 @@
 #include <RWStl.hxx>
 #include <RWObj.hxx>
 #include <RWObj_CafReader.hxx>
+#include <RWObj_CafWriter.hxx>
 #include <SelectMgr_SelectionManager.hxx>
 #include <Standard_ErrorHandler.hxx>
 #include <StdSelect_ViewerSelector3d.hxx>
@@ -669,6 +670,117 @@ static Standard_Integer ReadObj (Draw_Interpretor& theDI,
   return 0;
 }
 
+//=============================================================================
+//function : WriteObj
+//purpose  : Writes OBJ file
+//=============================================================================
+static Standard_Integer WriteObj (Draw_Interpretor& theDI,
+                                  Standard_Integer theNbArgs,
+                                  const char** theArgVec)
+{
+  TCollection_AsciiString anObjFilePath;
+  Handle(TDocStd_Document) aDoc;
+  Handle(TDocStd_Application) anApp = DDocStd::GetApplication();
+  TColStd_IndexedDataMapOfStringString aFileInfo;
+  Standard_Real aFileUnitFactor = -1.0;
+  RWMesh_CoordinateSystem aSystemCoordSys = RWMesh_CoordinateSystem_Zup, aFileCoordSys = RWMesh_CoordinateSystem_Yup;
+  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)
+      {
+        Message::SendFail() << "Syntax error: wrong length unit '" << aUnitStr << "'";
+        return 1;
+      }
+    }
+    else if (anArgIter + 1 < theNbArgs
+          && (anArgCase == "-filecoordinatesystem"
+           || anArgCase == "-filecoordsystem"
+           || anArgCase == "-filecoordsys"))
+    {
+      if (!parseCoordinateSystem (theArgVec[++anArgIter], aFileCoordSys))
+      {
+        Message::SendFail() << "Syntax error: unknown coordinate system '" << theArgVec[anArgIter] << "'";
+        return 1;
+      }
+    }
+    else if (anArgIter + 1 < theNbArgs
+          && (anArgCase == "-systemcoordinatesystem"
+           || anArgCase == "-systemcoordsystem"
+           || anArgCase == "-systemcoordsys"
+           || anArgCase == "-syscoordsys"))
+    {
+      if (!parseCoordinateSystem (theArgVec[++anArgIter], aSystemCoordSys))
+      {
+        Message::SendFail() << "Syntax error: unknown coordinate system '" << theArgVec[anArgIter] << "'";
+        return 1;
+      }
+    }
+    else if (anArgCase == "-comments"
+          && anArgIter + 1 < theNbArgs)
+    {
+      aFileInfo.Add ("Comments", theArgVec[++anArgIter]);
+    }
+    else if (anArgCase == "-author"
+          && anArgIter + 1 < theNbArgs)
+    {
+      aFileInfo.Add ("Author", theArgVec[++anArgIter]);
+    }
+    else if (aDoc.IsNull())
+    {
+      Standard_CString aNameVar = theArgVec[anArgIter];
+      DDocStd::GetDocument (aNameVar, aDoc, false);
+      if (aDoc.IsNull())
+      {
+        TopoDS_Shape aShape = DBRep::Get (aNameVar);
+        if (aShape.IsNull())
+        {
+          Message::SendFail() << "Syntax error: '" << aNameVar << "' is not a shape nor document";
+          return 1;
+        }
+
+        anApp->NewDocument (TCollection_ExtendedString ("BinXCAF"), aDoc);
+        Handle(XCAFDoc_ShapeTool) aShapeTool = XCAFDoc_DocumentTool::ShapeTool (aDoc->Main());
+        aShapeTool->AddShape (aShape);
+      }
+    }
+    else if (anObjFilePath.IsEmpty())
+    {
+      anObjFilePath = theArgVec[anArgIter];
+    }
+    else
+    {
+      Message::SendFail() << "Syntax error at '" << theArgVec[anArgIter] << "'";
+      return 1;
+    }
+  }
+  if (anObjFilePath.IsEmpty())
+  {
+    Message::SendFail() << "Syntax error: wrong number of arguments";
+    return 1;
+  }
+
+  Handle(Draw_ProgressIndicator) aProgress = new Draw_ProgressIndicator (theDI, 1);
+
+  const Standard_Real aSystemUnitFactor = UnitsMethods::GetCasCadeLengthUnit() * 0.001;
+  RWObj_CafWriter aWriter (anObjFilePath);
+  aWriter.ChangeCoordinateSystemConverter().SetInputLengthUnit (aSystemUnitFactor);
+  aWriter.ChangeCoordinateSystemConverter().SetInputCoordinateSystem (aSystemCoordSys);
+  aWriter.ChangeCoordinateSystemConverter().SetOutputLengthUnit (aFileUnitFactor);
+  aWriter.ChangeCoordinateSystemConverter().SetOutputCoordinateSystem (aFileCoordSys);
+  aWriter.Perform (aDoc, aFileInfo, aProgress->Start());
+  return 0;
+}
+
 static Standard_Integer writevrml
 (Draw_Interpretor& di, Standard_Integer argc, const char** argv)
 {
@@ -1782,18 +1894,20 @@ void  XSDRAWSTLVRML::InitCommands (Draw_Interpretor& theCommands)
                    "\n\t\t:                    (false by default)"
                    "\n\t\t:   -keepLate data is loaded into itself with preservation of information"
                    "\n\t\t:             about deferred storage to load/unload this data later.",
-                   "\n\t\t:   -toPrintDebugInfo print additional debug inforamtion during data reading"
+                   "\n\t\t:   -toPrintDebugInfo print additional debug information during data reading"
                    __FILE__, ReadGltf, g);
   theCommands.Add ("readgltf",
                    "readgltf shape file"
                    "\n\t\t: Same as ReadGltf but reads glTF file into a shape instead of a document.",
                    __FILE__, ReadGltf, g);
   theCommands.Add ("WriteGltf",
-                   "WriteGltf Doc file [-trsfFormat {compact|TRS|mat4}=compact] [-comments Text] [-author Name] [-forceUVExport] [-texturesSeparate]"
-                   "\n\t\t: Write XDE document into glTF file."
-                   "\n\t\t:   -trsfFormat preferred transformation format"
-                   "\n\t\t:   -forceUVExport always export UV coordinates"
-                   "\n\t\t:   -texturesSeparate write textures to separate files",
+                   "WriteGltf Doc file [-trsfFormat {compact|TRS|mat4}=compact]"
+           "\n\t\t:                    [-comments Text] [-author Name]"
+           "\n\t\t:                    [-forceUVExport] [-texturesSeparate]"
+           "\n\t\t: Write XDE document into glTF file."
+           "\n\t\t:   -trsfFormat preferred transformation format"
+           "\n\t\t:   -forceUVExport always export UV coordinates"
+           "\n\t\t:   -texturesSeparate write textures to separate files",
                    __FILE__, WriteGltf, g);
   theCommands.Add ("writegltf",
                    "writegltf shape file",
@@ -1826,6 +1940,18 @@ void  XSDRAWSTLVRML::InitCommands (Draw_Interpretor& theCommands)
            "\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 ("WriteObj",
+                   "WriteObj Doc file [-fileCoordSys {Zup|Yup}] [-fileUnit Unit]"
+           "\n\t\t:                   [-systemCoordSys {Zup|Yup}]"
+           "\n\t\t:                   [-comments Text] [-author Name]"
+           "\n\t\t: Write XDE document into OBJ file."
+           "\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:   -systemCoordSys system coordinate system; Zup when not specified.",
+                   __FILE__, WriteObj, g);
+  theCommands.Add ("writeobj",
+                   "writeobj shape file",
+                   __FILE__, WriteObj, 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 55f4aca..803b947 100644 (file)
@@ -1,6 +1,7 @@
 001 stl_read
 002 shape_write_stl
 003 gltf_read
-004 obj_read
-005 gltf_write
-006 gltf_lateload
+004 gltf_write
+005 gltf_lateload
+006 obj_read
+007 obj_write
diff --git a/tests/de_mesh/obj_write/as1 b/tests/de_mesh/obj_write/as1
new file mode 100644 (file)
index 0000000..4490648
--- /dev/null
@@ -0,0 +1,30 @@
+puts "========"
+puts "0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file"
+puts "Write as1 STEP model into OBJ file"
+puts "========"
+
+pload XDE OCAF MODELING VISUALIZATION
+Close D  -silent
+Close D1 -silent
+ReadStep D1 [locate_data_file as1-oc-214-mat.stp]
+XGetOneShape ss D1
+incmesh ss 1.0
+
+set aTmpObjBase "${imagedir}/${casename}_tmp"
+set aTmpObj "${aTmpObjBase}.obj"
+lappend occ_tmp_files $aTmpObj
+lappend occ_tmp_files "${aTmpObjBase}.mtl"
+lappend occ_tmp_files "${aTmpObjBase}_textures"
+
+WriteObj D1 "$aTmpObj"
+
+ReadObj D "$aTmpObj"
+XGetOneShape s D
+checknbshapes s -face 18 -compound 2
+
+vclear
+vinit View1
+XDisplay -dispMode 1 D
+vaxo
+vfit
+vdump ${imagedir}/${casename}.png
diff --git a/tests/de_mesh/obj_write/ball b/tests/de_mesh/obj_write/ball
new file mode 100644 (file)
index 0000000..81d586a
--- /dev/null
@@ -0,0 +1,28 @@
+puts "========"
+puts "0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file"
+puts "Write B-Rep model into OBJ file"
+puts "========"
+
+pload XDE OCAF MODELING VISUALIZATION
+Close D -silent
+
+restore [locate_data_file Ball.brep] b
+incmesh b 0.1
+
+set aTmpObjBase "${imagedir}/${casename}_tmp"
+set aTmpObj "${aTmpObjBase}.obj"
+lappend occ_tmp_files $aTmpObj
+lappend occ_tmp_files "${aTmpObjBase}.mtl"
+
+writeobj b "$aTmpObj"
+
+ReadObj D "$aTmpObj"
+XGetOneShape s D
+checknbshapes s -face 2 -compound 2
+
+vclear
+vinit View1
+XDisplay -dispMode 1 D
+vaxo
+vfit
+vdump ${imagedir}/${casename}.png
diff --git a/tests/de_mesh/obj_write/lantern b/tests/de_mesh/obj_write/lantern
new file mode 100644 (file)
index 0000000..d8b737c
--- /dev/null
@@ -0,0 +1,29 @@
+puts "========"
+puts "0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file"
+puts "Write textured lantern glTF model into OBJ file"
+puts "========"
+
+pload XDE OCAF MODELING VISUALIZATION
+Close D  -silent
+Close D1 -silent
+ReadGltf D1 [locate_data_file bug30691_Lantern.glb]
+
+set aTmpObjBase "${imagedir}/${casename}_tmp"
+set aTmpObj "${aTmpObjBase}.obj"
+lappend occ_tmp_files $aTmpObj
+lappend occ_tmp_files "${aTmpObjBase}.mtl"
+lappend occ_tmp_files "${aTmpObjBase}_textures"
+
+WriteObj D1 "$aTmpObj"
+
+ReadObj D "$aTmpObj"
+XGetOneShape s D
+checknbshapes s -face 3 -compound 1
+checktrinfo s -tri 5394 -nod 4145
+
+vclear
+vinit View1
+XDisplay -dispMode 1 D
+vaxo
+vfit
+vdump ${imagedir}/${casename}.png
diff --git a/tests/de_mesh/obj_write/mustang b/tests/de_mesh/obj_write/mustang
new file mode 100644 (file)
index 0000000..e4c4940
--- /dev/null
@@ -0,0 +1,29 @@
+puts "========"
+puts "0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file"
+puts "Write textured plane OBJ model into OBJ file"
+puts "========"
+
+pload XDE OCAF MODELING VISUALIZATION
+Close D  -silent
+Close D1 -silent
+ReadObj D1 [locate_data_file "P-51 Mustang.obj"]
+
+set aTmpObjBase "${imagedir}/${casename}_tmp"
+set aTmpObj "${aTmpObjBase}.obj"
+lappend occ_tmp_files $aTmpObj
+lappend occ_tmp_files "${aTmpObjBase}.mtl"
+lappend occ_tmp_files "${aTmpObjBase}_textures"
+
+WriteObj D1 "$aTmpObj"
+
+ReadObj D "$aTmpObj"
+XGetOneShape s D
+checknbshapes s -face 14 -compound 1
+checktrinfo s -tri 4309 -nod 4727
+
+vclear
+vinit View1
+XDisplay -dispMode 1 D
+vaxo
+vfit
+vdump ${imagedir}/${casename}.png
diff --git a/tests/de_mesh/obj_write/ship_boat b/tests/de_mesh/obj_write/ship_boat
new file mode 100644 (file)
index 0000000..efce97c
--- /dev/null
@@ -0,0 +1,29 @@
+puts "========"
+puts "0029303: Data Exchange - add RWObj_CafWriter tool for wavefront OBJ file"
+puts "Write textured boat OBJ model into OBJ file"
+puts "========"
+
+pload XDE OCAF MODELING VISUALIZATION
+Close D  -silent
+Close D1 -silent
+ReadObj D1 [locate_data_file ship_boat.obj]
+
+set aTmpObjBase "${imagedir}/${casename}_tmp"
+set aTmpObj "${aTmpObjBase}.obj"
+lappend occ_tmp_files $aTmpObj
+lappend occ_tmp_files "${aTmpObjBase}.mtl"
+lappend occ_tmp_files "${aTmpObjBase}_textures"
+
+WriteObj D1 "$aTmpObj"
+
+ReadObj D "$aTmpObj"
+XGetOneShape s D
+checknbshapes s -face 158 -compound 2
+checktrinfo s -tri 27297 -nod 40496
+
+vclear
+vinit View1
+XDisplay -dispMode 1 D
+vaxo
+vfit
+vdump ${imagedir}/${casename}.png
index 93d5109..ea409df 100644 (file)
@@ -7,19 +7,20 @@ pload MODELING VISUALIZATION
 box b1 1 1 1
 box b2 1 1 1
 
+vclear
 vinit View1
-vdisplay b1
-vdisplay b2
+vdisplay b1 b2
 vsetlocation b2 10 10 10
 vfit
 
 set listmem {}
-
-set i_max 3
-for {set i 1} {${i} <= ${i_max}} {incr i} {
-  vfps 1000
+set aNbChecks 50
+for {set anIter 1} {$anIter <= $aNbChecks} {incr anIter} {
+  vfps 100
   lappend listmem [meminfo h]
-  checktrend $listmem 0 1 "Memory leak detected"
+  #checktrend $listmem 0 1 "Memory leak detected"
 }
+puts $listmem
 
+checktrend $listmem 0 1 "Memory leak detected"
 vdump ${imagedir}/${casename}.png