// Created on: 1993-10-15 // Created by: Remi LEQUETTE // Copyright (c) 1993-1999 Matra Datavision // Copyright (c) 1999-2012 OPEN CASCADE SAS // // The content of this file is subject to the Open CASCADE Technology Public // License Version 6.5 (the "License"). You may not use the content of this file // except in compliance with the License. Please obtain a copy of the License // at http://www.opencascade.org and read it completely before using this file. // // The Initial Developer of the Original Code is Open CASCADE S.A.S., having its // main offices at: 1, place des Freres Montgolfier, 78280 Guyancourt, France. // // The Original Code and all software distributed under the License is // distributed on an "AS IS" basis, without warranty of any kind, and the // Initial Developer hereby disclaims all such warranties, including without // limitation, any warranties of merchantability, fitness for a particular // purpose or non-infringement. Please see the License for the specific terms // and conditions governing the rights and limitations under the License. #define TRC 0 #define MODIF 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include // sewing #include #include #include #include #include #include #include #include #include #include #ifdef DEB extern Standard_Boolean TopOpeBRepTool_GetcontextNOSEW(); #endif #define Opecom(st1,st2) (((st1)==TopAbs_IN) && ((st2)==TopAbs_IN)) #define Opefus(st1,st2) (((st1)==TopAbs_OUT) && ((st2)==TopAbs_OUT)) #define Opecut(st1,st2) (((st1)==TopAbs_OUT) && ((st2)==TopAbs_IN)) // ------------------------------------------------------------------- static void Sub_Classify(TopExp_Explorer& Ex, const TopAbs_State St1, TopTools_ListOfShape& Solids2, BRep_Builder& BB, TopTools_ListIteratorOfListOfShape& LIter, TopoDS_Shape& myShape); #ifdef DEB Standard_IMPORT Standard_Integer TopOpeBRepTool_BOOOPE_CHECK_DEB; #endif //modified by NIZHNY-MZV Wed Apr 19 17:19:11 2000 //see comments at the top of file TopOpeBRepBuild_Builder1.cxx //about using of this global variable extern Standard_Boolean GLOBAL_USE_NEW_BUILDER; // //modified by NIZNHY-PKV Sun Dec 15 17:17:56 2002 f extern void FDSCNX_Close();// see TopOpeBRepDS_connex.cxx extern void FDSSDM_Close();// see TopOpeBRepDS_samdom.cxx //======================================================================= //function : Delete //purpose : alias ~BRepAlgoAPI_BooleanOperation //======================================================================= void BRepAlgo_BooleanOperation::Delete() { FDSSDM_Close(); FDSCNX_Close(); } //modified by NIZNHY-PKV Sun Dec 15 17:17:58 2002 t //======================================================================= //function : BRepAlgoAPI_BooleanOperation //purpose : //======================================================================= BRepAlgo_BooleanOperation::BRepAlgo_BooleanOperation(const TopoDS_Shape& S1, const TopoDS_Shape& S2) : myS1(S1),myS2(S2),myBuilderCanWork(Standard_False) { TopOpeBRepDS_BuildTool BT; myHBuilder = new TopOpeBRepBuild_HBuilder(BT); } //======================================================================= //function : PerformDS //purpose : //======================================================================= void BRepAlgo_BooleanOperation::PerformDS() { // const Standard_Boolean CheckShapes = Standard_True; // create a data structure Handle(TopOpeBRepDS_HDataStructure) HDS; if (myHBuilder->DataStructure().IsNull()) HDS = new TopOpeBRepDS_HDataStructure(); else { HDS = myHBuilder->DataStructure(); HDS->ChangeDS().Init(); } #ifdef DEB TopOpeBRepDS_SettraceSPSX_HDS(HDS); #endif // fill the data Structure TopOpeBRep_DSFiller DSFiller; // define face/face intersection tolerances Standard_Boolean forcetoli = Standard_False; if (forcetoli) { Standard_Real tolarc=0,toltang=0; TopOpeBRep_ShapeIntersector& tobsi = DSFiller.ChangeShapeIntersector(); TopOpeBRep_FacesIntersector& tobfi = tobsi.ChangeFacesIntersector(); tobfi.ForceTolerances(tolarc,toltang); } DSFiller.Insert(myS1,myS2,HDS); // 020499 : JYL : reject if there is an edge of the SD // not coded sameparameter and not degenerated Standard_Boolean esp = HDS->EdgesSameParameter(); Standard_Boolean tede = Standard_True; if (!esp) { Standard_Integer i,n = HDS->NbShapes(); for (i = 1 ; i <= n; i++) { const TopoDS_Shape& s = HDS->Shape(i); if ( s.ShapeType() == TopAbs_EDGE ) { const TopoDS_Edge& e = TopoDS::Edge(s); Standard_Boolean sp = BRep_Tool::SameParameter(e); Standard_Boolean de = BRep_Tool::Degenerated(e); if ( !sp && !de ) { tede = Standard_False; break; } } } } myBuilderCanWork = (esp || tede) ; #ifdef DEB if (!esp) cout<<"BRepAlgo_BooleanOperation(DEB) some edges not SameParameter"<ChangeBuildTool(); TopOpeBRepTool_GeomTool& GTofBTofBuilder = BTofBuilder.ChangeGeomTool(); GTofBTofBuilder.SetTolerances(tol3dAPPROX,tol2dAPPROX); //modified by NIZHNY-MZV Thu Apr 20 09:35:44 2000 //see comments at the top of file TopOpeBRepBuild_Builder1.cxx //about using of this global variable GLOBAL_USE_NEW_BUILDER = Standard_True; myHBuilder->Perform(HDS,myS1,myS2); GLOBAL_USE_NEW_BUILDER = Standard_False; } //======================================================================= //function : Perform //purpose : //======================================================================= void BRepAlgo_BooleanOperation::Perform(const TopAbs_State St1, const TopAbs_State St2) { if ( ! BuilderCanWork() ) { return; } // modif JYL suite aux modifs LBR #if MODIF ... // on privilegie le traitement KPart (si c'en est un) // a tous les autres Standard_Integer kp = myHBuilder->IsKPart(); BRep_Builder BB; Standard_Boolean sewing = Standard_True; if ( kp ) { //modified by NIZHNY-MZV Thu Apr 20 09:34:33 2000 //see comments at the top of file TopOpeBRepBuild_Builder1.cxx //about using of this global variable GLOBAL_USE_NEW_BUILDER = Standard_True; myHBuilder->MergeKPart(St1,St2); GLOBAL_USE_NEW_BUILDER = Standard_False; BB.MakeCompound(TopoDS::Compound(myShape)); Done(); TopTools_ListIteratorOfListOfShape its(myHBuilder->Merged(myS1,St1)); for(; its.More(); its.Next()) BB.Add(myShape,its.Value()); } else { #if MODIF //====================================================================== //== Exploration of input shapes //== Creation of the list of solids //== Creation of the list of faces OUT OF solid //== Creation of the list of edges OUT OF face Standard_Integer nbs1,nbs2,nbf1,nbf2,nbe1,nbe2,nbv1,nbv2; TopTools_ListOfShape Solids1,Solids2,Faces1,Faces2,Edges1,Edges2,Vertex1,Vertex2; TopExp_Explorer Ex; for(Ex.Init(myS1,TopAbs_SOLID),nbs1=0; Ex.More(); Ex.Next()) { Solids1.Append(Ex.Current()); nbs1++; } for(Ex.Init(myS2,TopAbs_SOLID),nbs2=0; Ex.More(); Ex.Next()) { Solids2.Append(Ex.Current()); nbs2++; } //== Faces not in a solid for(Ex.Init(myS1,TopAbs_FACE,TopAbs_SOLID),nbf1=0; Ex.More(); Ex.Next()) { Faces1.Append(Ex.Current()); nbf1++; } for(Ex.Init(myS2,TopAbs_FACE,TopAbs_SOLID),nbf2=0; Ex.More(); Ex.Next()) { Faces2.Append(Ex.Current()); nbf2++; } //== Edges not in a solid for(Ex.Init(myS1,TopAbs_EDGE,TopAbs_FACE),nbe1=0; Ex.More(); Ex.Next()) { Edges1.Append(Ex.Current()); nbe1++; } for(Ex.Init(myS2,TopAbs_EDGE,TopAbs_FACE),nbe2=0; Ex.More(); Ex.Next()) { Edges2.Append(Ex.Current()); nbe2++; } //== Vertices not in an edge for(Ex.Init(myS1,TopAbs_VERTEX,TopAbs_EDGE),nbv1=0; Ex.More(); Ex.Next()) { Vertex1.Append(Ex.Current()); nbv1++; } for(Ex.Init(myS2,TopAbs_VERTEX,TopAbs_EDGE),nbv2=0; Ex.More(); Ex.Next()) { Vertex2.Append(Ex.Current()); nbv2++; } //-- cout<<"Solids1: "<0 && nbs2>0) && (nbe1 || nbe2 || nbf1 || nbf2 || nbv1 || nbv2)) { //-- cout<<"***** Not Yet Implemented : Compound of solid and non Solid"<Section(); for(; itloe.More(); itloe.Next()) BB.Add(myShape,itloe.Value()); } else { if(nbf1) { myHBuilder->MergeShapes(myS1,St1,SNULL,St2); for(LIter.Initialize(Faces1);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St1)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St1)); its.More();its.Next()) BB.Add(myShape,its.Value()); } else { const TopoDS_Shape& LV = LIter.Value(); if( (LV.Orientation() == TopAbs_EXTERNAL && St1==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St1==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St1,Solids2,BB,LIter,myShape); } //-- End Classification } } } // nbf1 SNULL.Nullify(); if ( Opefus(St1,St2) ) { if(nbf2) { myHBuilder->MergeShapes(SNULL,St1,myS2,St2); for(LIter.Initialize(Faces2);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St2)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St2)); its.More();its.Next()) BB.Add(myShape,its.Value()); } else { const TopoDS_Shape& LV = LIter.Value(); if( (LV.Orientation() == TopAbs_EXTERNAL && St2==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St2==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St2,Solids1,BB,LIter,myShape); } //-- End Classification } } } // nbf2 } // Fus } } // nbf1 && nbf2 else if (nbf1 || nbf2) { SNULL.Nullify(); if(nbf1) { myHBuilder->MergeShapes(myS1,St1,SNULL,St2); // modified by IFV for treating operation between shell and solid const TopTools_ListOfShape& MergedShapes = myHBuilder->Merged(myS1,St1); TopTools_IndexedMapOfShape aMapOfFaces; sewing = Standard_False; if(MergedShapes.Extent() != 0) { TopTools_ListIteratorOfListOfShape its(MergedShapes); for(; its.More(); its.Next()) { BB.Add(myShape,its.Value()); } TopExp::MapShapes(myShape, TopAbs_FACE, aMapOfFaces); } for(LIter.Initialize(Faces1);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St1)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St1)); its.More();its.Next()) { if(!aMapOfFaces.Contains(its.Value())) BB.Add(myShape,its.Value()); } } else { const TopoDS_Shape& LV = LIter.Value(); if(!aMapOfFaces.Contains(LV)) { if( (LV.Orientation() == TopAbs_EXTERNAL && St1==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St1==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St1,Solids2,BB,LIter,myShape); } //-- End Classification } } } } // nbf1 SNULL.Nullify(); if(nbf2) { myHBuilder->MergeShapes(SNULL,St1,myS2,St2); // modified by IFV for treating operation between shell and solid const TopTools_ListOfShape& MergedShapes = myHBuilder->Merged(myS2,St2); TopTools_IndexedMapOfShape aMapOfFaces; sewing = Standard_False; if(MergedShapes.Extent() != 0) { TopTools_ListIteratorOfListOfShape its(MergedShapes); for(; its.More(); its.Next()) { BB.Add(myShape,its.Value()); } TopExp::MapShapes(myShape, TopAbs_FACE, aMapOfFaces); } for(LIter.Initialize(Faces2);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St2)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St2)); its.More();its.Next()) { if(!aMapOfFaces.Contains(its.Value())) BB.Add(myShape,its.Value()); } } else { const TopoDS_Shape& LV = LIter.Value(); if(!aMapOfFaces.Contains(LV)) { if( (LV.Orientation() == TopAbs_EXTERNAL && St2==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St2==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St2,Solids1,BB,LIter,myShape); } //-- End Classification } } } } // nbf2 } // (nbf1 || nbf2) //---------------------------------------------------------------------- if(nbe1) { myHBuilder->MergeShapes(myS1,St1,SNULL,St2); for(LIter.Initialize(Edges1);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St1)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St1)); its.More();its.Next()) { BB.Add(myShape,its.Value()); } } else { const TopoDS_Shape& LV = LIter.Value(); if( (LV.Orientation() == TopAbs_EXTERNAL && St1==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St1==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St1,Solids2,BB,LIter,myShape); } //-- End Classification } } } if(nbe2) { myHBuilder->MergeShapes(SNULL,St1,myS2,St2); for(LIter.Initialize(Edges2);LIter.More();LIter.Next()) { if (myHBuilder->IsSplit(LIter.Value(),St2)) { TopTools_ListIteratorOfListOfShape its; for(its.Initialize(myHBuilder->Splits(LIter.Value(),St2)); its.More();its.Next()) { BB.Add(myShape,its.Value()); } } else { const TopoDS_Shape& LV = LIter.Value(); if( (LV.Orientation() == TopAbs_EXTERNAL && St2==TopAbs_OUT ) ||(LV.Orientation() == TopAbs_INTERNAL && St2==TopAbs_IN )) { BB.Add(myShape,LV); } else { //-- Classify : Sub_Classify(Ex,St2,Solids1,BB,LIter,myShape); } //-- End Classification } } } //---------------------------------------------------------------------- //-- V1:Vertex1 state1 = OUT -> Preserve V1 if V1 is Out all S2 //-- V1:Vertex1 state1 = IN -> Preserve V1 if V1 is In one of S2 if(nbv1 && nbs2) { if(St1 == TopAbs_IN) { for(LIter.Initialize(Vertex1);LIter.More();LIter.Next()) { Standard_Boolean keep = Standard_False; Standard_Boolean ok = Standard_True; const TopoDS_Vertex& V=TopoDS::Vertex(LIter.Value()); gp_Pnt P=BRep_Tool::Pnt(V); Standard_Real Tol = BRep_Tool::Tolerance(V); TopTools_ListIteratorOfListOfShape SIter; for(SIter.Initialize(Solids2); SIter.More() && ok==Standard_True; SIter.Next()) { BRepClass3d_SolidClassifier SolClass(SIter.Value()); SolClass.Perform(P,Tol); if(SolClass.State() == TopAbs_IN) { ok=Standard_False; keep = Standard_True; } } if(keep) { BB.Add(myShape,LIter.Value()); } } } else { if(St1 == TopAbs_OUT) { for(LIter.Initialize(Vertex1);LIter.More();LIter.Next()) { Standard_Boolean keep = Standard_True; Standard_Boolean ok = Standard_True; const TopoDS_Vertex& V=TopoDS::Vertex(LIter.Value()); gp_Pnt P=BRep_Tool::Pnt(V); Standard_Real Tol = BRep_Tool::Tolerance(V); TopTools_ListIteratorOfListOfShape SIter; for(SIter.Initialize(Solids2); SIter.More() && ok==Standard_True; SIter.Next()) { BRepClass3d_SolidClassifier SolClass(SIter.Value()); SolClass.Perform(P,Tol); if(SolClass.State() != TopAbs_OUT) { keep = Standard_False; ok = Standard_False; } } if(keep) { BB.Add(myShape,LIter.Value()); } } } } } if(nbv2 && nbs1) { if(St2 == TopAbs_IN) { for(LIter.Initialize(Vertex2);LIter.More();LIter.Next()) { Standard_Boolean keep = Standard_False; Standard_Boolean ok = Standard_True; const TopoDS_Vertex& V=TopoDS::Vertex(LIter.Value()); gp_Pnt P=BRep_Tool::Pnt(V); Standard_Real Tol = BRep_Tool::Tolerance(V); TopTools_ListIteratorOfListOfShape SIter; for(SIter.Initialize(Solids1); SIter.More() && ok==Standard_True; SIter.Next()) { BRepClass3d_SolidClassifier SolClass(SIter.Value()); SolClass.Perform(P,Tol); if(SolClass.State() == TopAbs_IN) { ok=Standard_False; keep = Standard_True; } } if(keep) { BB.Add(myShape,LIter.Value()); } } } else { if(St2 == TopAbs_OUT) { for(LIter.Initialize(Vertex2);LIter.More();LIter.Next()) { Standard_Boolean keep = Standard_True; Standard_Boolean ok = Standard_True; const TopoDS_Vertex& V=TopoDS::Vertex(LIter.Value()); gp_Pnt P=BRep_Tool::Pnt(V); Standard_Real Tol = BRep_Tool::Tolerance(V); TopTools_ListIteratorOfListOfShape SIter; for(SIter.Initialize(Solids1); SIter.More() && ok==Standard_True; SIter.Next()) { BRepClass3d_SolidClassifier SolClass(SIter.Value()); SolClass.Perform(P,Tol); if(SolClass.State() != TopAbs_OUT) { keep = Standard_False; ok = Standard_False; } } if(keep) { BB.Add(myShape,LIter.Value()); } } } } } if(nbs1 && nbs2 ) { myHBuilder->MergeShapes(myS1,St1,myS2,St2); if(myHBuilder->IsMerged(myS1,St1)) { TopTools_ListIteratorOfListOfShape its; its = myHBuilder->Merged(myS1,St1); Standard_Integer nbSolids = 0; for(; its.More(); its.Next(), nbSolids++) { BB.Add(myShape,its.Value()); } } } #else myHBuilder->MergeSolids(myS1,St1,myS2,St2); TopTools_ListIteratorOfListOfShape its; BB.MakeCompound(TopoDS::Compound(myShape)); its = myHBuilder->Merged(myS1,St1); while (its.More()) { BB.Add(myShape,its.Value()); its.Next(); } #endif // #if MODIF } // Creation of the Map used in IsDeleted. TopExp_Explorer ex; ex.Init(myShape,TopAbs_FACE); for (; ex.More(); ex.Next()) myMap.Add(ex.Current()); ex.Init(myShape,TopAbs_EDGE); // for FRIKO for (; ex.More(); ex.Next()) myMap.Add(ex.Current()); // Checking same parameter of new edges of section Standard_Real eTol,cTol; for (myHBuilder->InitSection(1); myHBuilder->MoreSection(); myHBuilder->NextSection()) { const TopoDS_Shape& cur = myHBuilder->CurrentSection(); if (cur.ShapeType()==TopAbs_EDGE) { BRepCheck_Edge bce(TopoDS::Edge(cur)); cTol=bce.Tolerance(); eTol = BRep_Tool::Tolerance(TopoDS::Edge(cur)); if (eTolChangeBuildTool(); TopOpeBRepTool_GeomTool& GTofBTofBuilder = BTofBuilder.ChangeGeomTool(); GTofBTofBuilder.Define(TopOpeBRepTool_APPROX); GTofBTofBuilder.DefineCurves(Standard_True); GTofBTofBuilder.DefinePCurves1(Standard_True); GTofBTofBuilder.DefinePCurves2(Standard_True); } //======================================================================= //function : Modified //purpose : //======================================================================= const TopTools_ListOfShape& BRepAlgo_BooleanOperation::Modified(const TopoDS_Shape& S) { myGenerated.Clear(); TopTools_MapOfShape aMap; // to check if shape can be added in list more then one time aMap.Clear(); if (myHBuilder->IsSplit(S, TopAbs_OUT)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Splits(S, TopAbs_OUT)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } if (myHBuilder->IsSplit(S, TopAbs_IN)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Splits(S, TopAbs_IN)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } if (myHBuilder->IsSplit(S, TopAbs_ON)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Splits(S, TopAbs_ON)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } if (myHBuilder->IsMerged(S, TopAbs_OUT)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Merged(S, TopAbs_OUT)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } if (myHBuilder->IsMerged(S, TopAbs_IN)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Merged(S, TopAbs_IN)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } if (myHBuilder->IsMerged(S, TopAbs_ON)) { TopTools_ListIteratorOfListOfShape It(myHBuilder->Merged(S, TopAbs_ON)); for(;It.More();It.Next()) { if (topToSew.IsBound(It.Value())) {if(aMap.Add(topToSew.Find(It.Value()))) myGenerated.Append(topToSew.Find(It.Value()));} else {if(aMap.Add(It.Value())) myGenerated.Append(It.Value());} } } return myGenerated; } //======================================================================= //function : IsDeleted //purpose : //======================================================================= Standard_Boolean BRepAlgo_BooleanOperation::IsDeleted(const TopoDS_Shape& S) { Standard_Boolean Deleted = Standard_True; if (myMap.Contains(S) || myHBuilder->IsMerged(S, TopAbs_OUT) || myHBuilder->IsMerged(S, TopAbs_IN) || myHBuilder->IsMerged(S, TopAbs_ON) || myHBuilder->IsSplit (S, TopAbs_OUT) || myHBuilder->IsSplit (S, TopAbs_IN) || myHBuilder->IsSplit (S, TopAbs_ON)) return Standard_False; return Deleted; }