Salome HOME
Task #3231: Sketcher Offset of a curve.
authorArtem Zhidkov <Artem.Zhidkov@opencascade.com>
Sat, 4 Jul 2020 07:45:53 +0000 (10:45 +0300)
committerArtem Zhidkov <Artem.Zhidkov@opencascade.com>
Sat, 4 Jul 2020 07:45:53 +0000 (10:45 +0300)
Fix issues

src/GeomAlgoAPI/GeomAlgoAPI_WireBuilder.cpp
src/GeomAlgoAPI/GeomAlgoAPI_WireBuilder.h
src/SketchPlugin/CMakeLists.txt
src/SketchPlugin/SketchPlugin_Offset.cpp
src/SketchPlugin/Test/TestOffset.py [deleted file]
src/SketchPlugin/Test/TestOffset1.py [new file with mode: 0644]
src/SketchPlugin/Test/TestOffset2.py [new file with mode: 0644]

index 53bc674a4bfc4954add5d3e0e073eae2c76d6ff8..a90d1e95320afd3bb302da0592dde07c4f5e9133 100644 (file)
 #include <GeomAPI_Vertex.h>
 #include <GeomAPI_ShapeExplorer.h>
 
+#include <BRep_Builder.hxx>
 #include <BRep_Tool.hxx>
 #include <BRepBuilderAPI_MakeWire.hxx>
+#include <BRepTools_ReShape.hxx>
 #include <Geom_Curve.hxx>
 #include <TopoDS.hxx>
 #include <TopoDS_Wire.hxx>
+#include <TopExp.hxx>
 #include <TopExp_Explorer.hxx>
 
 static GeomShapePtr fromTopoDS(const TopoDS_Shape& theShape)
@@ -38,7 +41,8 @@ static GeomShapePtr fromTopoDS(const TopoDS_Shape& theShape)
   return aResultShape;
 }
 
-GeomAlgoAPI_WireBuilder::GeomAlgoAPI_WireBuilder(const ListOfShape& theShapes)
+GeomAlgoAPI_WireBuilder::GeomAlgoAPI_WireBuilder(const ListOfShape& theShapes,
+                                                 const bool theForceOpenWire)
 {
   TopTools_ListOfShape aListOfEdges;
 
@@ -61,14 +65,68 @@ GeomAlgoAPI_WireBuilder::GeomAlgoAPI_WireBuilder(const ListOfShape& theShapes)
     }
   }
 
+  bool isSplitWire = false;
+  gp_Pnt aSplitPoint;
+  if (theForceOpenWire) {
+    // find a vertex to split the wire
+    TopoDS_Vertex V1[2];
+    TopExp::Vertices(TopoDS::Edge(aListOfEdges.First()), V1[0], V1[1]);
+    TopoDS_Vertex V2[2];
+    TopExp::Vertices(TopoDS::Edge(aListOfEdges.Last()), V2[0], V2[1]);
+    gp_Pnt P1[2] = { BRep_Tool::Pnt(V1[0]), BRep_Tool::Pnt(V1[1]) };
+    gp_Pnt P2[2] = { BRep_Tool::Pnt(V2[0]), BRep_Tool::Pnt(V2[1]) };
+    double Tol1[2] = { BRep_Tool::Tolerance(V1[0]), BRep_Tool::Tolerance(V1[1]) };
+    double Tol2[2] = { BRep_Tool::Tolerance(V2[0]), BRep_Tool::Tolerance(V2[1]) };
+    for (int i = 0; i < 2 && !isSplitWire; ++i)
+      for (int j = 0; j < 2 && !isSplitWire; ++j)
+        if (P1[i].Distance(P2[i]) < Max(Tol1[i], Tol2[j])) {
+          aSplitPoint = P1[i];
+          isSplitWire = true;
+        }
+  }
+
   BRepBuilderAPI_MakeWire* aWireBuilder = new BRepBuilderAPI_MakeWire;
   aWireBuilder->Add(aListOfEdges);
   if (aWireBuilder->Error() == BRepBuilderAPI_WireDone) {
     setImpl(aWireBuilder);
     setBuilderType(OCCT_BRepBuilderAPI_MakeShape);
 
-    // store generated/modified shapes
+    // split the result wire
     TopoDS_Wire aWire = aWireBuilder->Wire();
+    if (isSplitWire && BRep_Tool::IsClosed(aWire)) {
+      TopoDS_Wire aNewWire;
+      BRep_Builder aBuilder;
+      aBuilder.MakeWire(aNewWire);
+      for (TopExp_Explorer anExp(aWire, TopAbs_EDGE); anExp.More(); anExp.Next()) {
+        TopoDS_Edge aNewCurrent = TopoDS::Edge(anExp.Current());
+        if (isSplitWire) {
+          bool isToReshape = false;
+          BRepTools_ReShape aReshape;
+          TopoDS_Vertex aVF, aVL;
+          TopExp::Vertices(aNewCurrent, aVF, aVL);
+          gp_Pnt aPF = BRep_Tool::Pnt(aVF);
+          double aTolF = BRep_Tool::Tolerance(aVF);
+          gp_Pnt aPL = BRep_Tool::Pnt(aVL);
+          double aTolL = BRep_Tool::Tolerance(aVL);
+          if (aSplitPoint.SquareDistance(aPF) < aTolF * aTolF) {
+            aReshape.Replace(aVF, aReshape.CopyVertex(aVF));
+            isToReshape = true;
+          }
+          else if (aSplitPoint.SquareDistance(aPL) < aTolL * aTolL) {
+            aReshape.Replace(aVL, aReshape.CopyVertex(aVL));
+            isToReshape = true;
+          }
+          if (isToReshape) {
+            aNewCurrent = TopoDS::Edge(aReshape.Apply(aNewCurrent));
+            isSplitWire = false; // no need to continue splitting
+          }
+        }
+        aBuilder.Add(aNewWire, aNewCurrent);
+      }
+      aWire = aNewWire;
+    }
+
+    // store generated/modified shapes
     for (TopTools_ListOfShape::Iterator aBaseIt(aListOfEdges); aBaseIt.More(); aBaseIt.Next()) {
       TopoDS_Edge aBaseCurrent = TopoDS::Edge(aBaseIt.Value());
       Standard_Real aFirst, aLast;
index 6824c155051018f51ed2ec881963f93f7db38232..9c4370c5f756e60bc5cee2dd1954064fe3c8a75a 100644 (file)
@@ -33,9 +33,12 @@ class GeomAlgoAPI_WireBuilder : public GeomAlgoAPI_MakeShapeCustom
  public:
    /// \brief Creates a wire from edges and wires.
    /// \param[in] theShapes list of shapes. Only edges and wires allowed.
+   /// \param[in] theForceOpenWire indicates the necessity to split wire
+   ///                             in the first vertex if it becomes closed.
    /// The edges are not to be consecutive.
    /// But they are to be all connected geometrically or topologically.
-   GEOMALGOAPI_EXPORT GeomAlgoAPI_WireBuilder(const ListOfShape& theShapes);
+   GEOMALGOAPI_EXPORT GeomAlgoAPI_WireBuilder(const ListOfShape& theShapes,
+                                              const bool theForceOpenWire = false);
 
    /// \brief Creates a wire from edges and wires.
    /// \param[in] theShapes list of shapes. Only edges and wires allowed.
index 59f65d25755d39675af80876405c1ef417fb89c3..401570245dde24d36d4026504408a52bad3c3730 100644 (file)
@@ -328,7 +328,8 @@ ADD_UNIT_TESTS(
   TestMultiRotation05.py
   TestMultiRotationWithParameter.py
   TestMultiTranslation.py
-  TestOffset.py
+  TestOffset1.py
+  TestOffset2.py
   TestPresentation.py
   TestProjection.py
   TestProjectionBSpline.py
index 7ea47ee0e485820786b84000a2044284410158a5..f3f8d2b891e543bdc309c36822ff4c5cad3774d5 100644 (file)
@@ -143,10 +143,14 @@ void SketchPlugin_Offset::execute()
       // 5.b. Find a chain of edges
       std::list<FeaturePtr> aChain;
       aChain.push_back(aFeature);
-      if (aStartPoint && anEndPoint) { // not closed edge
-        bool isClosed = findWireOneWay(aFeature, aFeature, aStartPoint, anEdgesSet, aProcessedEdgesSet, aChain, true);
-        if (!isClosed)
-          findWireOneWay(aFeature, aFeature, anEndPoint, anEdgesSet, aProcessedEdgesSet, aChain, false);
+      bool isClosed = !(aStartPoint && anEndPoint);  // not closed edge
+      if (!isClosed) {
+        isClosed = findWireOneWay(aFeature, aFeature, aStartPoint, anEdgesSet,
+                                  aProcessedEdgesSet, aChain, true);
+        if (!isClosed) {
+          isClosed = findWireOneWay(aFeature, aFeature, anEndPoint, anEdgesSet,
+                                    aProcessedEdgesSet, aChain, false);
+        }
       }
       aProcessedEdgesSet.insert(aFeature);
 
@@ -161,7 +165,7 @@ void SketchPlugin_Offset::execute()
         }
       }
       std::shared_ptr<GeomAlgoAPI_WireBuilder> aWireBuilder(
-          new GeomAlgoAPI_WireBuilder(aTopoChain));
+          new GeomAlgoAPI_WireBuilder(aTopoChain, !isClosed));
 
       GeomShapePtr aWireShape = aWireBuilder->shape();
       GeomWirePtr aWire (new GeomAPI_Wire (aWireShape));
@@ -283,7 +287,8 @@ bool SketchPlugin_Offset::findWireOneWay (const FeaturePtr& theFirstEdge,
   }
 
   // 5. Continue gathering the chain (recursive)
-  return findWireOneWay (theFirstEdge, aNextEdgeFeature, aP2, theEdgesSet, theProcessedEdgesSet, theChain, isPrepend);
+  return findWireOneWay (theFirstEdge, aNextEdgeFeature, aP2, theEdgesSet,
+                         theProcessedEdgesSet, theChain, isPrepend);
 }
 
 static void setRefListValue(AttributeRefListPtr theList, int theListSize,
@@ -647,8 +652,27 @@ void SketchPlugin_Offset::mkBSpline (FeaturePtr& theResult,
 
 void SketchPlugin_Offset::attributeChanged(const std::string& theID)
 {
-////  if (theID == EDGES_ID())
-////    removeCreated();
+  if (theID == EDGES_ID()) {
+    AttributeRefListPtr aSelected = reflist(EDGES_ID());
+    if (aSelected->size() == 0) {
+      // Clear list of objects
+      AttributeRefListPtr anOffsetAttr = reflist(SketchPlugin_Constraint::ENTITY_B());
+      std::list<ObjectPtr> anOffsetList = anOffsetAttr->list();
+      std::set<FeaturePtr> aFeaturesToBeRemoved;
+      for (std::list<ObjectPtr>::iterator anIt = anOffsetList.begin();
+           anIt != anOffsetList.end(); ++anIt) {
+        FeaturePtr aFeature = ModelAPI_Feature::feature(*anIt);
+        if (aFeature)
+          aFeaturesToBeRemoved.insert(aFeature);
+      }
+
+      reflist(SketchPlugin_Constraint::ENTITY_A())->clear();
+      anOffsetAttr->clear();
+      intArray(SketchPlugin_Constraint::ENTITY_C())->setSize(0);
+
+      ModelAPI_Tools::removeFeaturesAndReferences(aFeaturesToBeRemoved);
+    }
+  }
 }
 
 bool SketchPlugin_Offset::customAction(const std::string& theActionId)
@@ -701,9 +725,12 @@ bool SketchPlugin_Offset::findWires()
 
       std::list<FeaturePtr> aChain;
       aChain.push_back(aFeature);
-      bool isClosed = findWireOneWay(aFeature, aFeature, aStartPoint, anEdgesSet, aProcessedEdgesSet, aChain, true);
-      if (!isClosed)
-        findWireOneWay(aFeature, aFeature, anEndPoint, anEdgesSet, aProcessedEdgesSet, aChain, false);
+      bool isClosed = findWireOneWay(aFeature, aFeature, aStartPoint, anEdgesSet,
+                                     aProcessedEdgesSet, aChain, true);
+      if (!isClosed) {
+        findWireOneWay(aFeature, aFeature, anEndPoint, anEdgesSet,
+                       aProcessedEdgesSet, aChain, false);
+      }
 
       std::list<FeaturePtr>::iterator aChainIt = aChain.begin();
       for (; aChainIt != aChain.end(); ++aChainIt) {
diff --git a/src/SketchPlugin/Test/TestOffset.py b/src/SketchPlugin/Test/TestOffset.py
deleted file mode 100644 (file)
index b673927..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-# Copyright (C) 2014-2019  CEA/DEN, EDF R&D
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
-#
-# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
-#
-
-"""
-    TestOffset.py
-    Unit test of SketchPlugin_Offset class
-
-    SketchPlugin_Offset
-        static const std::string ID("SketchOffset");
-        data()->addAttribute(EDGES_ID(), ModelAPI_AttributeRefList::typeId());
-        data()->addAttribute(VALUE_ID(), ModelAPI_AttributeDouble::typeId());
-        data()->addAttribute(REVERSED_ID(), ModelAPI_AttributeBoolean::typeId());
-        data()->addAttribute(SketchPlugin_Constraint::ENTITY_A(), ModelAPI_AttributeRefList::typeId());
-        data()->addAttribute(SketchPlugin_Constraint::ENTITY_B(), ModelAPI_AttributeRefList::typeId());
-        data()->addAttribute(SketchPlugin_Constraint::ENTITY_C(), ModelAPI_AttributeIntArray::typeId());
-"""
-
-from GeomDataAPI import *
-from ModelAPI import *
-import math
-from salome.shaper import model
-
-#=========================================================================
-# Initialization of the test
-#=========================================================================
-
-__updated__ = "2020-06-30"
-
-#=========================================================================
-# Auxiliary functions
-#=========================================================================
-def normalize(theDir):
-    aLen = math.hypot(theDir[0], theDir[1])
-    if aLen < 1.e-10:
-        aLen = 1.0
-    return [theDir[0] / aLen, theDir[1] / aLen]
-
-def checkOffset(theListIn, theListOut, theOutToIn, theDist, isReversed, nbIn, nbOut):
-    TOL = 6.e-5
-    aNbIn  = theListIn.size()
-    aNbOut = theListOut.size()
-
-    #print("**checkOffset**")
-    assert (theListIn.size() == nbIn)
-    assert (theListOut.size() == nbOut)
-    assert (theOutToIn.size() == nbOut)
-
-    for ind in range(0, aNbOut):
-        aFeatureOut = ModelAPI_Feature.feature(theListOut.object(ind))
-        assert(aFeatureOut is not None)
-        anInInd = theOutToIn.value(ind)
-        if (anInInd == -1):
-            assert(aFeatureOut.getKind() == "SketchArc")
-        else:
-            aFeatureIn = ModelAPI_Feature.feature(theListIn.object(anInInd))
-            assert(aFeatureIn is not None)
-
-            #print(aFeatureIn.getKind())
-            if (aFeatureIn.getKind() == "SketchLine"):
-                assert(aFeatureOut.getKind() == aFeatureIn.getKind())
-                # Line and its offset are parallel
-                aP1Out = geomDataAPI_Point2D(aFeatureOut.attribute('StartPoint'))
-                aP2Out = geomDataAPI_Point2D(aFeatureOut.attribute('EndPoint'))
-                aP1In  = geomDataAPI_Point2D(aFeatureIn.attribute('StartPoint'))
-                aP2In  = geomDataAPI_Point2D(aFeatureIn.attribute('EndPoint'))
-                aDirOut = [aP2Out.x() - aP1Out.x(), aP2Out.y() - aP1Out.y()]
-                aDirIn  = [ aP2In.x() -  aP1In.x(),  aP2In.y() -  aP1In.y()]
-                aCross = aDirOut[0] * aDirIn[1] - aDirOut[1] * aDirIn[0]
-                assert math.fabs(aCross) < TOL, "aCross = {0}".format(aCross)
-            elif (aFeatureIn.getKind() == "SketchArc"):
-                assert(aFeatureOut.getKind() == aFeatureIn.getKind())
-                # Arc and its offset have the same center
-                aCPOut = geomDataAPI_Point2D(aFeatureOut.attribute('center_point'))
-                aCPIn  = geomDataAPI_Point2D(aFeatureIn.attribute('center_point'))
-                assert (math.fabs(aCPOut.x() - aCPIn.x()) < TOL)
-                assert (math.fabs(aCPOut.y() - aCPIn.y()) < TOL)
-
-
-#=========================================================================
-# Start of test
-#=========================================================================
-aSession = ModelAPI_Session.get()
-aDocument = aSession.moduleDocument()
-#=========================================================================
-# Creation of a sketch
-#=========================================================================
-aSession.startOperation()
-aSketchCommonFeature = aDocument.addFeature("Sketch")
-aSketchFeature = featureToCompositeFeature(aSketchCommonFeature)
-origin = geomDataAPI_Point(aSketchFeature.attribute("Origin"))
-origin.setValue(0, 0, 0)
-dirx = geomDataAPI_Dir(aSketchFeature.attribute("DirX"))
-dirx.setValue(1, 0, 0)
-norm = geomDataAPI_Dir(aSketchFeature.attribute("Norm"))
-norm.setValue(0, 0, 1)
-aSession.finishOperation()
-#=========================================================================
-# Creation of an arc and two lines
-#=========================================================================
-# Arc
-aSession.startOperation()
-aSketchArc1 = aSketchFeature.addFeature("SketchArc")
-anArcCentr = geomDataAPI_Point2D(aSketchArc1.attribute("center_point"))
-anArcCentr.setValue(10., 10.)
-anArcStartPoint = geomDataAPI_Point2D(aSketchArc1.attribute("start_point"))
-anArcStartPoint.setValue(50., 0.)
-anArcEndPoint = geomDataAPI_Point2D(aSketchArc1.attribute("end_point"))
-anArcEndPoint.setValue(0., 50.)
-aSession.finishOperation()
-# Line 1
-aSession.startOperation()
-aSketchLine1 = aSketchFeature.addFeature("SketchLine")
-aLine1StartPoint = geomDataAPI_Point2D(aSketchLine1.attribute("StartPoint"))
-aLine1EndPoint = geomDataAPI_Point2D(aSketchLine1.attribute("EndPoint"))
-aLine1StartPoint.setValue(0., 50.)
-aLine1EndPoint.setValue(-20., 0.)
-aSession.finishOperation()
-# Line 2
-aSession.startOperation()
-aSketchLine2 = aSketchFeature.addFeature("SketchLine")
-aLine2StartPoint = geomDataAPI_Point2D(aSketchLine2.attribute("StartPoint"))
-aLine2EndPoint = geomDataAPI_Point2D(aSketchLine2.attribute("EndPoint"))
-aLine2StartPoint.setValue(50., 0.)
-aLine2EndPoint.setValue(-20., 0.)
-aSession.finishOperation()
-assert (model.dof(aSketchFeature) == 13)
-#=========================================================================
-# Link arc points and lines points by the coincidence constraint
-#=========================================================================
-aSession.startOperation()
-aConstraint = aSketchFeature.addFeature("SketchConstraintCoincidence")
-reflistA = aConstraint.refattr("ConstraintEntityA")
-reflistB = aConstraint.refattr("ConstraintEntityB")
-reflistA.setAttr(anArcEndPoint)
-reflistB.setAttr(aLine1StartPoint)
-aConstraint.execute()
-aSession.finishOperation()
-aSession.startOperation()
-aConstraint = aSketchFeature.addFeature("SketchConstraintCoincidence")
-reflistA = aConstraint.refattr("ConstraintEntityA")
-reflistB = aConstraint.refattr("ConstraintEntityB")
-reflistA.setAttr(anArcStartPoint)
-reflistB.setAttr(aLine2StartPoint)
-aConstraint.execute()
-aSession.finishOperation()
-assert (model.dof(aSketchFeature) == 9)
-#=========================================================================
-# Make offset for objects created above
-#=========================================================================
-VALUE = 13
-IS_REVERSED = False
-aSession.startOperation()
-anOffset = aSketchFeature.addFeature("SketchOffset")
-aRefListInitial = anOffset.reflist("segments")
-aRefListInitial.append(aSketchLine1.lastResult())
-aRefListInitial.append(aSketchArc1.lastResult())
-aRefListInitial.append(aSketchLine2.lastResult())
-anOffset.real("offset_value").setValue(VALUE)
-anOffset.boolean("reversed").setValue(IS_REVERSED)
-anOffset.execute()
-aSession.finishOperation()
-assert (model.dof(aSketchFeature) == 9)
-#=========================================================================
-# Verify all offset objects
-#=========================================================================
-aRefListA = anOffset.reflist("ConstraintEntityA")
-aRefListB = anOffset.reflist("ConstraintEntityB")
-anOffsetToBaseMap = anOffset.intArray("ConstraintEntityC")
-checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 3, 6)
-assert (model.dof(aSketchFeature) == 9)
-
-#=========================================================================
-# Remove object from offset
-#=========================================================================
-aSession.startOperation()
-aRefListInitial.remove(aSketchLine2.lastResult())
-aSession.finishOperation()
-checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 2, 4)
-assert (model.dof(aSketchFeature) == 9)
-
-#=========================================================================
-# Clear list of objects
-#=========================================================================
-aSession.startOperation()
-aRefListInitial.clear()
-#TODO: uncomment next line
-#checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 0, 0)
-# add arc once again
-aRefListInitial.append(aSketchArc1.lastResult())
-aSession.finishOperation()
-checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 1, 1)
-assert (model.dof(aSketchFeature) == 9)
-
-#=========================================================================
-# End of test
-#=========================================================================
-
-assert(model.checkPythonDump())
diff --git a/src/SketchPlugin/Test/TestOffset1.py b/src/SketchPlugin/Test/TestOffset1.py
new file mode 100644 (file)
index 0000000..24b1622
--- /dev/null
@@ -0,0 +1,222 @@
+# Copyright (C) 2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+
+"""
+    TestOffset.py
+    Unit test of SketchPlugin_Offset class
+
+    SketchPlugin_Offset
+        static const std::string ID("SketchOffset");
+        data()->addAttribute(EDGES_ID(), ModelAPI_AttributeRefList::typeId());
+        data()->addAttribute(VALUE_ID(), ModelAPI_AttributeDouble::typeId());
+        data()->addAttribute(REVERSED_ID(), ModelAPI_AttributeBoolean::typeId());
+        data()->addAttribute(SketchPlugin_Constraint::ENTITY_A(), ModelAPI_AttributeRefList::typeId());
+        data()->addAttribute(SketchPlugin_Constraint::ENTITY_B(), ModelAPI_AttributeRefList::typeId());
+        data()->addAttribute(SketchPlugin_Constraint::ENTITY_C(), ModelAPI_AttributeIntArray::typeId());
+"""
+
+from GeomDataAPI import *
+from ModelAPI import *
+import math
+from salome.shaper import model
+
+#=========================================================================
+# Initialization of the test
+#=========================================================================
+
+__updated__ = "2020-06-30"
+
+#=========================================================================
+# Auxiliary functions
+#=========================================================================
+def normalize(theDir):
+    aLen = math.hypot(theDir[0], theDir[1])
+    if aLen < 1.e-10:
+        aLen = 1.0
+    return [theDir[0] / aLen, theDir[1] / aLen]
+
+def checkOffset(theListIn, theListOut, theOutToIn, theDist, isReversed, nbIn, nbOut):
+    TOL = 6.e-5
+    aNbIn  = theListIn.size()
+    aNbOut = theListOut.size()
+
+    #print("**checkOffset**")
+    assert (theListIn.size() == nbIn)
+    assert (theListOut.size() == nbOut)
+    assert (theOutToIn.size() == nbOut)
+
+    for ind in range(0, aNbOut):
+        aFeatureOut = ModelAPI_Feature.feature(theListOut.object(ind))
+        assert(aFeatureOut is not None)
+        anInInd = theOutToIn.value(ind)
+        if (anInInd == -1):
+            assert(aFeatureOut.getKind() == "SketchArc")
+        else:
+            aFeatureIn = ModelAPI_Feature.feature(theListIn.object(anInInd))
+            assert(aFeatureIn is not None)
+
+            #print(aFeatureIn.getKind())
+            if (aFeatureIn.getKind() == "SketchLine"):
+                assert(aFeatureOut.getKind() == aFeatureIn.getKind())
+                # Line and its offset are parallel
+                aP1Out = geomDataAPI_Point2D(aFeatureOut.attribute('StartPoint'))
+                aP2Out = geomDataAPI_Point2D(aFeatureOut.attribute('EndPoint'))
+                aP1In  = geomDataAPI_Point2D(aFeatureIn.attribute('StartPoint'))
+                aP2In  = geomDataAPI_Point2D(aFeatureIn.attribute('EndPoint'))
+                aDirOut = [aP2Out.x() - aP1Out.x(), aP2Out.y() - aP1Out.y()]
+                aDirIn  = [ aP2In.x() -  aP1In.x(),  aP2In.y() -  aP1In.y()]
+                aCross = aDirOut[0] * aDirIn[1] - aDirOut[1] * aDirIn[0]
+                assert math.fabs(aCross) < TOL, "aCross = {0}".format(aCross)
+            elif (aFeatureIn.getKind() == "SketchArc"):
+                assert(aFeatureOut.getKind() == aFeatureIn.getKind())
+                # Arc and its offset have the same center
+                aCPOut = geomDataAPI_Point2D(aFeatureOut.attribute('center_point'))
+                aCPIn  = geomDataAPI_Point2D(aFeatureIn.attribute('center_point'))
+                assert (math.fabs(aCPOut.x() - aCPIn.x()) < TOL)
+                assert (math.fabs(aCPOut.y() - aCPIn.y()) < TOL)
+
+
+#=========================================================================
+# Start of test
+#=========================================================================
+aSession = ModelAPI_Session.get()
+aDocument = aSession.moduleDocument()
+#=========================================================================
+# Creation of a sketch
+#=========================================================================
+aSession.startOperation()
+aSketchCommonFeature = aDocument.addFeature("Sketch")
+aSketchFeature = featureToCompositeFeature(aSketchCommonFeature)
+origin = geomDataAPI_Point(aSketchFeature.attribute("Origin"))
+origin.setValue(0, 0, 0)
+dirx = geomDataAPI_Dir(aSketchFeature.attribute("DirX"))
+dirx.setValue(1, 0, 0)
+norm = geomDataAPI_Dir(aSketchFeature.attribute("Norm"))
+norm.setValue(0, 0, 1)
+aSession.finishOperation()
+#=========================================================================
+# Creation of an arc and two lines
+#=========================================================================
+# Arc
+aSession.startOperation()
+aSketchArc1 = aSketchFeature.addFeature("SketchArc")
+anArcCentr = geomDataAPI_Point2D(aSketchArc1.attribute("center_point"))
+anArcCentr.setValue(10., 10.)
+anArcStartPoint = geomDataAPI_Point2D(aSketchArc1.attribute("start_point"))
+anArcStartPoint.setValue(50., 0.)
+anArcEndPoint = geomDataAPI_Point2D(aSketchArc1.attribute("end_point"))
+anArcEndPoint.setValue(0., 50.)
+aSession.finishOperation()
+# Line 1
+aSession.startOperation()
+aSketchLine1 = aSketchFeature.addFeature("SketchLine")
+aLine1StartPoint = geomDataAPI_Point2D(aSketchLine1.attribute("StartPoint"))
+aLine1EndPoint = geomDataAPI_Point2D(aSketchLine1.attribute("EndPoint"))
+aLine1StartPoint.setValue(0., 50.)
+aLine1EndPoint.setValue(-20., 0.)
+aSession.finishOperation()
+# Line 2
+aSession.startOperation()
+aSketchLine2 = aSketchFeature.addFeature("SketchLine")
+aLine2StartPoint = geomDataAPI_Point2D(aSketchLine2.attribute("StartPoint"))
+aLine2EndPoint = geomDataAPI_Point2D(aSketchLine2.attribute("EndPoint"))
+aLine2StartPoint.setValue(50., 0.)
+aLine2EndPoint.setValue(-20., 0.)
+aSession.finishOperation()
+assert (model.dof(aSketchFeature) == 13)
+#=========================================================================
+# Link arc points and lines points by the coincidence constraints
+#=========================================================================
+aSession.startOperation()
+aConstraint = aSketchFeature.addFeature("SketchConstraintCoincidence")
+reflistA = aConstraint.refattr("ConstraintEntityA")
+reflistB = aConstraint.refattr("ConstraintEntityB")
+reflistA.setAttr(anArcEndPoint)
+reflistB.setAttr(aLine1StartPoint)
+aConstraint.execute()
+aSession.finishOperation()
+aSession.startOperation()
+aConstraint = aSketchFeature.addFeature("SketchConstraintCoincidence")
+reflistA = aConstraint.refattr("ConstraintEntityA")
+reflistB = aConstraint.refattr("ConstraintEntityB")
+reflistA.setAttr(anArcStartPoint)
+reflistB.setAttr(aLine2StartPoint)
+aConstraint.execute()
+aSession.finishOperation()
+aSession.startOperation()
+aConstraint = aSketchFeature.addFeature("SketchConstraintCoincidence")
+reflistA = aConstraint.refattr("ConstraintEntityA")
+reflistB = aConstraint.refattr("ConstraintEntityB")
+reflistA.setAttr(aLine1EndPoint)
+reflistB.setAttr(aLine2EndPoint)
+aConstraint.execute()
+aSession.finishOperation()
+assert (model.dof(aSketchFeature) == 7)
+#=========================================================================
+# Make offset for objects created above
+#=========================================================================
+VALUE = 13
+IS_REVERSED = False
+aSession.startOperation()
+anOffset = aSketchFeature.addFeature("SketchOffset")
+aRefListInitial = anOffset.reflist("segments")
+aRefListInitial.append(aSketchLine1.lastResult())
+aRefListInitial.append(aSketchArc1.lastResult())
+aRefListInitial.append(aSketchLine2.lastResult())
+anOffset.real("offset_value").setValue(VALUE)
+anOffset.boolean("reversed").setValue(IS_REVERSED)
+anOffset.execute()
+aSession.finishOperation()
+assert (model.dof(aSketchFeature) == 7)
+#=========================================================================
+# Verify all offset objects
+#=========================================================================
+aRefListA = anOffset.reflist("ConstraintEntityA")
+aRefListB = anOffset.reflist("ConstraintEntityB")
+anOffsetToBaseMap = anOffset.intArray("ConstraintEntityC")
+checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 3, 6)
+assert (model.dof(aSketchFeature) == 7)
+
+#=========================================================================
+# Remove object from offset
+#=========================================================================
+aSession.startOperation()
+aRefListInitial.remove(aSketchLine2.lastResult())
+aSession.finishOperation()
+checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 2, 4)
+assert (model.dof(aSketchFeature) == 7)
+
+#=========================================================================
+# Clear list of objects
+#=========================================================================
+aSession.startOperation()
+aRefListInitial.clear()
+#TODO: uncomment next line
+#checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 0, 0)
+# add arc once again
+aRefListInitial.append(aSketchArc1.lastResult())
+aSession.finishOperation()
+checkOffset(aRefListA, aRefListB, anOffsetToBaseMap, VALUE, IS_REVERSED, 1, 1)
+assert (model.dof(aSketchFeature) == 7)
+
+#=========================================================================
+# End of test
+#=========================================================================
+
+assert(model.checkPythonDump())
diff --git a/src/SketchPlugin/Test/TestOffset2.py b/src/SketchPlugin/Test/TestOffset2.py
new file mode 100644 (file)
index 0000000..e15e35d
--- /dev/null
@@ -0,0 +1,61 @@
+# Copyright (C) 2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+
+from salome.shaper import model
+
+SKETCH_DOF = 9
+
+model.begin()
+partSet = model.moduleDocument()
+
+### Create Sketch
+Sketch_1 = model.addSketch(partSet, model.defaultPlane("XOY"))
+
+### Create SketchArc
+SketchArc_1 = Sketch_1.addArc(10, 10, 50, 0, 0, 50, False)
+
+### Create SketchLine
+SketchLine_1 = Sketch_1.addLine(0, 50, -20, 0)
+
+### Create SketchLine
+SketchLine_2 = Sketch_1.addLine(50, 0, -20, 0)
+Sketch_1.setCoincident(SketchArc_1.endPoint(), SketchLine_1.startPoint())
+Sketch_1.setCoincident(SketchArc_1.startPoint(), SketchLine_2.startPoint())
+model.do()
+
+assert(model.dof(Sketch_1) == SKETCH_DOF)
+
+### Create SketchOffset
+SketchOffset_1_objects = [SketchLine_1.result(), SketchArc_1.results()[1], SketchLine_2.result()]
+SketchOffset_1 = Sketch_1.addOffset(SketchOffset_1_objects, 13, False)
+model.do()
+
+# DoF should not change
+assert(model.dof(Sketch_1) == SKETCH_DOF)
+# check number of features
+assert(len(SketchOffset_1.offset()) == 5)
+model.testNbSubFeatures(Sketch_1, "SketchPoint", 0)
+model.testNbSubFeatures(Sketch_1, "SketchLine", 4)
+model.testNbSubFeatures(Sketch_1, "SketchArc", 4)
+model.testNbSubFeatures(Sketch_1, "SketchBSpline", 0)
+model.testNbSubFeatures(Sketch_1, "SketchBSplinePeriodic", 0)
+
+model.end()
+
+assert(model.checkPythonDump())