From c837befca5d8a28a000ac1b87345f038d87b727f Mon Sep 17 00:00:00 2001 From: Alexey Kondratyev Date: Wed, 27 Apr 2022 11:51:19 +0300 Subject: [PATCH] bos #29479 Show edges directions Add algorithm to show direction of edges. --- doc/gui/General/Introduction.rst | 7 ++ src/Model/Model_Data.cpp | 3 +- src/ModelAPI/ModelAPI_Result.cpp | 2 + src/ModelAPI/ModelAPI_Result.h | 8 ++ src/ModelAPI/ModelAPI_Tools.cpp | 25 ++++++ src/ModelAPI/ModelAPI_Tools.h | 8 ++ src/ModuleBase/ModuleBase_ArrowPrs.cpp | 113 ++++++++++++++++++++++++ src/ModuleBase/ModuleBase_ArrowPrs.h | 72 +++++++++++++++ src/ModuleBase/ModuleBase_ResultPrs.cpp | 47 ++++++++++ src/XGUI/XGUI_ContextMenuMgr.cpp | 17 ++++ src/XGUI/XGUI_Workshop.cpp | 28 ++++++ src/XGUI/XGUI_Workshop.h | 3 + src/XGUI/XGUI_msg_fr.ts | 4 + src/XGUI/XGUI_pictures.qrc | 1 + src/XGUI/pictures/edges_dir.png | Bin 0 -> 456 bytes 15 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 src/ModuleBase/ModuleBase_ArrowPrs.cpp create mode 100644 src/ModuleBase/ModuleBase_ArrowPrs.h create mode 100644 src/XGUI/pictures/edges_dir.png diff --git a/doc/gui/General/Introduction.rst b/doc/gui/General/Introduction.rst index b691bdf2a..8da63702f 100644 --- a/doc/gui/General/Introduction.rst +++ b/doc/gui/General/Introduction.rst @@ -394,6 +394,13 @@ This point of view can be modified using viewer commands: **Panning**, **Zooming Two view windows +The viewer is able to show direction of edges of objects. + +.. figure:: /images/edges_directions.png + :align: center + + Showing the edges direction + The description of OCC 3D Viewer architecture and functionality is provided in GUI module user's guide in chapter **OCC 3D Viewer**. .. _parameter_usage: diff --git a/src/Model/Model_Data.cpp b/src/Model/Model_Data.cpp index 058b326e6..00e6defd2 100644 --- a/src/Model/Model_Data.cpp +++ b/src/Model/Model_Data.cpp @@ -465,7 +465,8 @@ void Model_Data::sendAttributeUpdated(ModelAPI_Attribute* theAttr) // trim: need to redisplay or set color in the python script if (myObject && (theAttr->attributeType() == "Point2D" || theAttr->id() == "Color" || theAttr->id() == "Transparency" || theAttr->id() == "Deflection" || - theAttr->id() == "Iso_lines" || theAttr->id() == "Show_Iso_lines")) { + theAttr->id() == "Iso_lines" || theAttr->id() == "Show_Iso_lines" || + theAttr->id() == "Show_Edges_direction")) { static const Events_ID anEvent = Events_Loop::eventByName(EVENT_OBJECT_TO_REDISPLAY); ModelAPI_EventCreator::get()->sendUpdated(myObject, anEvent); } diff --git a/src/ModelAPI/ModelAPI_Result.cpp b/src/ModelAPI/ModelAPI_Result.cpp index ec27cbf82..a225fe950 100644 --- a/src/ModelAPI/ModelAPI_Result.cpp +++ b/src/ModelAPI/ModelAPI_Result.cpp @@ -41,6 +41,8 @@ void ModelAPI_Result::initAttributes() aData->addAttribute(ISO_LINES_ID(), ModelAPI_AttributeIntArray::typeId())->setIsArgument(false); aData->addAttribute(SHOW_ISO_LINES_ID(), ModelAPI_AttributeBoolean::typeId())-> setIsArgument(false); + aData->addAttribute(SHOW_EDGES_DIRECTION_ID(), ModelAPI_AttributeBoolean::typeId())-> + setIsArgument(false); } bool ModelAPI_Result::setDisabled(std::shared_ptr theThis, const bool theFlag) diff --git a/src/ModelAPI/ModelAPI_Result.h b/src/ModelAPI/ModelAPI_Result.h index df12ecaf6..778529c22 100644 --- a/src/ModelAPI/ModelAPI_Result.h +++ b/src/ModelAPI/ModelAPI_Result.h @@ -79,6 +79,14 @@ class ModelAPI_Result : public ModelAPI_Object return MY_SHOW_ISO_LINES_ID; } + /// Reference to the transparency of the result. + /// The double value is used. The value is in [0, 1] range + inline static const std::string& SHOW_EDGES_DIRECTION_ID() + { + static const std::string MY_SHOW_EDGES_DIRECTION_ID("Show_Edges_direction"); + return MY_SHOW_EDGES_DIRECTION_ID; + } + /// Returns true if the result is concealed from the data tree (referenced by other objects) MODELAPI_EXPORT virtual bool isConcealed(); diff --git a/src/ModelAPI/ModelAPI_Tools.cpp b/src/ModelAPI/ModelAPI_Tools.cpp index 703404069..84c5de685 100644 --- a/src/ModelAPI/ModelAPI_Tools.cpp +++ b/src/ModelAPI/ModelAPI_Tools.cpp @@ -1169,6 +1169,31 @@ bool isShownIsoLines(std::shared_ptr theResult) return false; } +//****************************************************** +void showEdgesDirection(std::shared_ptr theResult, bool theShow) +{ + if (!theResult.get()) + return; + + AttributeBooleanPtr aAttr = theResult->data()->boolean(ModelAPI_Result::SHOW_EDGES_DIRECTION_ID()); + if (aAttr.get() != NULL) { + aAttr->setValue(theShow); + } +} + +//****************************************************** +bool isShowEdgesDirection(std::shared_ptr theResult) +{ + if (!theResult.get()) + return false; + + AttributeBooleanPtr aAttr = theResult->data()->boolean(ModelAPI_Result::SHOW_EDGES_DIRECTION_ID()); + if (aAttr.get() != NULL) { + return aAttr->value(); + } + return false; +} + //************************************************************** void setTransparency(ResultPtr theResult, double theTransparency) { diff --git a/src/ModelAPI/ModelAPI_Tools.h b/src/ModelAPI/ModelAPI_Tools.h index 0ad692341..83a1e010e 100644 --- a/src/ModelAPI/ModelAPI_Tools.h +++ b/src/ModelAPI/ModelAPI_Tools.h @@ -304,6 +304,14 @@ MODELAPI_EXPORT void showIsoLines(std::shared_ptr theResult, bo MODELAPI_EXPORT bool isShownIsoLines(std::shared_ptr theResult); +/*! Set visibility of edges direction +* \param[in] theResult a result object +* \param[in] theShow is a visibility flag +*/ +MODELAPI_EXPORT void showEdgesDirection(std::shared_ptr theResult, bool theShow); + +MODELAPI_EXPORT bool isShowEdgesDirection(std::shared_ptr theResult); + /*! Returns current transparency in the given result * \param theResult a result object * \return a transparency value or -1 if it was not defined diff --git a/src/ModuleBase/ModuleBase_ArrowPrs.cpp b/src/ModuleBase/ModuleBase_ArrowPrs.cpp new file mode 100644 index 000000000..da02554e6 --- /dev/null +++ b/src/ModuleBase/ModuleBase_ArrowPrs.cpp @@ -0,0 +1,113 @@ +// Copyright (C) 2014-2022 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 +// + +#include "ModuleBase_ArrowPrs.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +IMPLEMENT_STANDARD_RTTIEXT(ModuleBase_ArrowPrs, AIS_InteractiveContext) + + +ModuleBase_ArrowPrs::ModuleBase_ArrowPrs(const Handle(V3d_Viewer)& theViewer, + const GeomEdgePtr& theEdge) + : AIS_InteractiveContext(theViewer), + myEdge(theEdge) +{ +} + +//******************************************************************** +void ModuleBase_ArrowPrs::DrawArrow(const Handle(Prs3d_Presentation)& thePrs, + Quantity_Color theColor) +{ + Handle(Prs3d_Drawer) aDrawer = myDefaultDrawer; + Handle(Prs3d_ArrowAspect) anArrowAspect = aDrawer->ArrowAspect(); + + Handle(Graphic3d_AspectLine3d) PtA = anArrowAspect->Aspect(); + PtA->SetColor(theColor); + + Handle(Graphic3d_Group) TheGroup = thePrs->CurrentGroup(); + TheGroup->SetPrimitivesAspect(PtA); + + TopoDS_Vertex aV1, aV2; + TopoDS_Edge anEdgeE = myEdge->impl(); + anEdgeE.Orientation(TopAbs_FORWARD); + if (anEdgeE.IsNull()) return; + + TopExp::Vertices(anEdgeE, aV1, aV2); + gp_Pnt aP1 = BRep_Tool::Pnt(aV1); + gp_Pnt aP2 = BRep_Tool::Pnt(aV2); + + double fp, lp; + gp_Vec aDirVec; + Handle(Geom_Curve) C = BRep_Tool::Curve(anEdgeE, fp, lp); + + if (C.IsNull()) return; + + if (anEdgeE.Orientation() == TopAbs_FORWARD) + C->D1(lp, aP2, aDirVec); + else { + C->D1(fp, aP1, aDirVec); + aP2 = aP1; + } + + GeomAdaptor_Curve aAdC; + aAdC.Load(C, fp, lp); + Standard_Real aDist = GCPnts_AbscissaPoint::Length(aAdC, fp, lp); + + if (aDist > gp::Resolution()) { + gp_Dir aDir; + if (anEdgeE.Orientation() == TopAbs_FORWARD) + aDir = aDirVec; + else + aDir = -aDirVec; + + TopoDS_Vertex aVertex; + BRep_Builder aB; + aB.MakeVertex(aVertex, aP2, Precision::Confusion()); + Prs3d_Arrow::Draw(TheGroup, aP2, aDir, M_PI / 180. * 5., aDist / 10.); + } +} + +//******************************************************************** +bool ModuleBase_ArrowPrs::Comparator::operator()(const std::shared_ptr& theEdge1, + const std::shared_ptr& theEdge2) const +{ + const TopoDS_Edge& aShape1 = theEdge1->impl(); + const TopoDS_Edge& aShape2 = theEdge2->impl(); + bool isLess = aShape1.TShape() < aShape2.TShape(); + if (aShape1.TShape() == aShape2.TShape()) { + Standard_Integer aHash1 = aShape1.Location().HashCode(IntegerLast()); + Standard_Integer aHash2 = aShape2.Location().HashCode(IntegerLast()); + isLess = aHash1 < aHash2; + } + return isLess; +} diff --git a/src/ModuleBase/ModuleBase_ArrowPrs.h b/src/ModuleBase/ModuleBase_ArrowPrs.h new file mode 100644 index 000000000..238bca1d0 --- /dev/null +++ b/src/ModuleBase/ModuleBase_ArrowPrs.h @@ -0,0 +1,72 @@ +// Copyright (C) 2014-2022 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 +// + +#ifndef ModuleBase_ArrowPrs_H +#define ModuleBase_ArrowPrs_H + +#include +#include +#include + +#include + +DEFINE_STANDARD_HANDLE(ModuleBase_ArrowPrs, AIS_InteractiveContext) + +/** +* \ingroup GUI +* A presentation class for displaying a direction of edge +*/ + +class ModuleBase_ArrowPrs : public AIS_InteractiveContext +{ +public: + /// Constructor + /// \param theViewer a viewer which theEdge is displaying. + /// \param theEdge an edge whose direction to display. + Standard_EXPORT ModuleBase_ArrowPrs(const Handle(V3d_Viewer)& theViewer, + const GeomEdgePtr& theEdge); + + /// Returns an edge shape + GeomEdgePtr Edge() const { return myEdge; } + + /// Draw arrow that represent direction of the edge. + Standard_EXPORT void DrawArrow(const Handle(Prs3d_Presentation)& thePrs, Quantity_Color theColor); + + /// \brief Compare addresses of edges + class Comparator + { + public: + /// Return \c true if the address of the first edge is less than the address of the second + MODULEBASE_EXPORT + bool operator ()(const std::shared_ptr& theEdge1, + const std::shared_ptr& theEdge2) const; + }; + + DEFINE_STANDARD_RTTIEXT(ModuleBase_ArrowPrs, AIS_InteractiveContext) + +private: + + /// The edge whose direction to display. + GeomEdgePtr myEdge; +}; + +typedef std::pair EdgeDirection; +typedef std::map EdgesDirectionMap; + +#endif \ No newline at end of file diff --git a/src/ModuleBase/ModuleBase_ResultPrs.cpp b/src/ModuleBase/ModuleBase_ResultPrs.cpp index c9cf04473..ccba3a207 100644 --- a/src/ModuleBase/ModuleBase_ResultPrs.cpp +++ b/src/ModuleBase/ModuleBase_ResultPrs.cpp @@ -61,6 +61,10 @@ #include #include #include +#include +#include +#include +#include #if OCC_VERSION_HEX > 0x070400 #include @@ -300,6 +304,49 @@ void ModuleBase_ResultPrs::Compute( catch (...) { return; } + if (myResult.get() && ModelAPI_Tools::isShowEdgesDirection(myResult)) + { + TopExp_Explorer Exp(myshape, TopAbs_EDGE); + for (; Exp.More(); Exp.Next()) { + TopoDS_Edge anEdgeE = TopoDS::Edge(Exp.Current()); + if (anEdgeE.IsNull()) + continue; + + // draw curve direction (issue 0021087) + anEdgeE.Orientation(TopAbs_FORWARD); + + TopoDS_Vertex aV1, aV2; + TopExp::Vertices(anEdgeE, aV1, aV2); + gp_Pnt aP1 = BRep_Tool::Pnt(aV1); + gp_Pnt aP2 = BRep_Tool::Pnt(aV2); + + double fp, lp; + gp_Vec aDirVec; + Handle(Geom_Curve) C = BRep_Tool::Curve(anEdgeE, fp, lp); + + if (C.IsNull()) continue; + + if (anEdgeE.Orientation() == TopAbs_FORWARD) + C->D1(lp, aP2, aDirVec); + else { + C->D1(fp, aP1, aDirVec); + aP2 = aP1; + } + GeomAdaptor_Curve aAdC; + aAdC.Load(C, fp, lp); + Standard_Real aDist = GCPnts_AbscissaPoint::Length(aAdC, fp, lp); + + if (aDist > gp::Resolution()) { + gp_Dir aDir; + if (anEdgeE.Orientation() == TopAbs_FORWARD) + aDir = aDirVec; + else + aDir = -aDirVec; + + Prs3d_Arrow::Draw(thePresentation->CurrentGroup(), aP2, aDir, M_PI / 180.*5., aDist / 10.); + } + } + } // visualize hidden sub-shapes transparent if (myResult.get()) { diff --git a/src/XGUI/XGUI_ContextMenuMgr.cpp b/src/XGUI/XGUI_ContextMenuMgr.cpp index 64c590e66..e42131d55 100644 --- a/src/XGUI/XGUI_ContextMenuMgr.cpp +++ b/src/XGUI/XGUI_ContextMenuMgr.cpp @@ -157,6 +157,11 @@ void XGUI_ContextMenuMgr::createActions() aDesktop); addAction("WIREFRAME_CMD", anAction); + anAction = ModuleBase_Tools::createAction(QIcon(":pictures/edges_dir.png"), tr("Show edges direction"), + aDesktop); + anAction->setCheckable(true); + addAction("SHOW_EDGES_DIRECTION_CMD", anAction); + anAction = ModuleBase_Tools::createAction(QIcon(":pictures/iso_lines.png"), tr("Define Isos..."), aDesktop); addAction("ISOLINES_CMD", anAction); @@ -340,6 +345,9 @@ void XGUI_ContextMenuMgr::updateObjectBrowserMenu() action("WIREFRAME_CMD")->setEnabled(true); action("SHADING_CMD")->setEnabled(true); } + action("SHOW_EDGES_DIRECTION_CMD")->setEnabled(true); + action("SHOW_EDGES_DIRECTION_CMD")->setChecked(ModelAPI_Tools::isShowEdgesDirection(aResult)); + action("SHOW_ISOLINES_CMD")->setEnabled(true); action("SHOW_ISOLINES_CMD")->setChecked(ModelAPI_Tools::isShownIsoLines(aResult)); action("ISOLINES_CMD")->setEnabled(true); @@ -396,6 +404,7 @@ void XGUI_ContextMenuMgr::updateObjectBrowserMenu() action("SHOW_ONLY_CMD")->setEnabled(true); action("SHADING_CMD")->setEnabled(true); action("WIREFRAME_CMD")->setEnabled(true); + action("SHOW_EDGES_DIRECTION_CMD")->setEnabled(true); action("SHOW_ISOLINES_CMD")->setEnabled(true); action("ISOLINES_CMD")->setEnabled(true); } @@ -590,6 +599,10 @@ void XGUI_ContextMenuMgr::updateViewerMenu() if (aResult.get()) { action("SHOW_ISOLINES_CMD")->setEnabled(true); action("SHOW_ISOLINES_CMD")->setChecked(ModelAPI_Tools::isShownIsoLines(aResult)); + + action("SHOW_EDGES_DIRECTION_CMD")->setEnabled(true); + action("SHOW_EDGES_DIRECTION_CMD")->setChecked( + ModelAPI_Tools::isShowEdgesDirection(aResult)); } } } @@ -696,6 +709,7 @@ void XGUI_ContextMenuMgr::buildObjBrowserMenu() aList.clear(); aList.append(action("WIREFRAME_CMD")); aList.append(action("SHADING_CMD")); + aList.append(action("SHOW_EDGES_DIRECTION_CMD")); aList.append(mySeparator1); // this separator is not shown as this action is added after show only // qt list container contains only one instance of the same action aList.append(action("SHOW_CMD")); @@ -720,6 +734,7 @@ void XGUI_ContextMenuMgr::buildObjBrowserMenu() aList.clear(); aList.append(action("WIREFRAME_CMD")); aList.append(action("SHADING_CMD")); + aList.append(action("SHOW_EDGES_DIRECTION_CMD")); aList.append(mySeparator1); // this separator is not shown as this action is added after show only // qt list container contains only one instance of the same action aList.append(action("SHOW_CMD")); @@ -801,6 +816,7 @@ void XGUI_ContextMenuMgr::buildViewerMenu() aList.clear(); aList.append(action("WIREFRAME_CMD")); aList.append(action("SHADING_CMD")); + aList.append(action("SHOW_EDGES_DIRECTION_CMD")); aList.append(mySeparator2); aList.append(action("COLOR_CMD")); aList.append(action("DEFLECTION_CMD")); @@ -845,6 +861,7 @@ void XGUI_ContextMenuMgr::addObjBrowserMenu(QMenu* theMenu) const } else if (aSelected > 1) { anActions.append(action("WIREFRAME_CMD")); anActions.append(action("SHADING_CMD")); + anActions.append(action("SHOW_EDGES_DIRECTION_CMD")); anActions.append(mySeparator1); anActions.append(action("SHOW_CMD")); anActions.append(action("HIDE_CMD")); diff --git a/src/XGUI/XGUI_Workshop.cpp b/src/XGUI/XGUI_Workshop.cpp index 126c07066..8859ef654 100644 --- a/src/XGUI/XGUI_Workshop.cpp +++ b/src/XGUI/XGUI_Workshop.cpp @@ -1822,6 +1822,8 @@ void XGUI_Workshop::onContextMenuCommand(const QString& theId, bool isChecked) setDisplayMode(anObjects, XGUI_Displayer::Shading); else if (theId == "WIREFRAME_CMD") setDisplayMode(anObjects, XGUI_Displayer::Wireframe); + else if (theId == "SHOW_EDGES_DIRECTION_CMD") + toggleEdgesDirection(anObjects); else if (theId == "HIDEALL_CMD") { QObjectPtrList aList = myDisplayer->displayedObjects(); foreach (ObjectPtr aObj, aList) { @@ -3055,6 +3057,32 @@ void XGUI_Workshop::setDisplayMode(const QObjectPtrList& theList, int theMode) myDisplayer->updateViewer(); } +//************************************************************** +void XGUI_Workshop::toggleEdgesDirection(const QObjectPtrList& theList) +{ + foreach(ObjectPtr anObj, theList) { + ResultPtr aResult = std::dynamic_pointer_cast(anObj); + if (aResult.get() != NULL) + { + bool aToShow = !ModelAPI_Tools::isShowEdgesDirection(aResult); + ResultBodyPtr aBodyResult = std::dynamic_pointer_cast(aResult); + if (aBodyResult.get() != NULL) { // change property for all sub-solids + std::list allRes; + ModelAPI_Tools::allSubs(aBodyResult, allRes); + std::list::iterator aRes; + for (aRes = allRes.begin(); aRes != allRes.end(); aRes++) { + ModelAPI_Tools::showEdgesDirection(*aRes, aToShow); + myDisplayer->redisplay(*aRes, false); + } + } + ModelAPI_Tools::showEdgesDirection(aResult, aToShow); + myDisplayer->redisplay(anObj, false); + } + } + if (theList.size() > 0) + myDisplayer->updateViewer(); +} + //************************************************************** void XGUI_Workshop::closeDocument() { diff --git a/src/XGUI/XGUI_Workshop.h b/src/XGUI/XGUI_Workshop.h index 19234656d..81145343e 100644 --- a/src/XGUI/XGUI_Workshop.h +++ b/src/XGUI/XGUI_Workshop.h @@ -236,6 +236,9 @@ Q_OBJECT /// \param theMode a mode to set (see \ref XGUI_Displayer) void setDisplayMode(const QObjectPtrList& theList, int theMode); + /// Toggle visualisation of edges direction + void toggleEdgesDirection(const QObjectPtrList& theList); + /// Set selection mode in viewer. If theMode=-1 then activate default mode /// \param theMode the selection mode (according to TopAbs_ShapeEnum) void setViewerSelectionMode(int theMode); diff --git a/src/XGUI/XGUI_msg_fr.ts b/src/XGUI/XGUI_msg_fr.ts index 4f5f1ee3b..16f7ea842 100644 --- a/src/XGUI/XGUI_msg_fr.ts +++ b/src/XGUI/XGUI_msg_fr.ts @@ -228,6 +228,10 @@ Recover Récupérer + + Show edges direction + Afficher la direction des bords + XGUI_DataTree diff --git a/src/XGUI/XGUI_pictures.qrc b/src/XGUI/XGUI_pictures.qrc index 2b81d5e48..f598306b4 100644 --- a/src/XGUI/XGUI_pictures.qrc +++ b/src/XGUI/XGUI_pictures.qrc @@ -98,5 +98,6 @@ pictures/CrossCursor.png pictures/HandCursor.png pictures/iso_lines.png + pictures/edges_dir.png diff --git a/src/XGUI/pictures/edges_dir.png b/src/XGUI/pictures/edges_dir.png new file mode 100644 index 0000000000000000000000000000000000000000..bff61628e98cff7ad874653e1df1468369b270f2 GIT binary patch literal 456 zcmV;(0XP1MP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGh)c^nu)d4-$Sn&V=0bNN%K~y+T#nZbh z#bF%B@t<4nq9n;;Q!F-xQj8X{7%;FgNSXWx6d9E=ut>4ErBjqbF^Log_W?z2DcAR# zbL3LkeCqZ4J)LuY&+~o0zvl%1odhp&h;i`8=@s5@F@9vBW~yOvlCpGK@8BAsZhP zAHKiGedWMI6-gWH{ur#}*SPmcYH