diff --git a/src/Mod/AddonManager b/src/Mod/AddonManager index 24249b9c29..34d433a02c 160000 --- a/src/Mod/AddonManager +++ b/src/Mod/AddonManager @@ -1 +1 @@ -Subproject commit 24249b9c298d9d23556b1edf1a5b76fa0dfe4de7 +Subproject commit 34d433a02c7ec5c73bec9c57d0a27ea70b36c90d diff --git a/src/Mod/Sketcher/App/Constraint.cpp b/src/Mod/Sketcher/App/Constraint.cpp index 5b66154279..0fc5fe0a2e 100644 --- a/src/Mod/Sketcher/App/Constraint.cpp +++ b/src/Mod/Sketcher/App/Constraint.cpp @@ -24,9 +24,16 @@ #ifndef _PreComp_ #include #include +#include #include +#include +#include +#include +#include #endif +#include + #include #include #include @@ -44,21 +51,6 @@ using namespace Base; TYPESYSTEM_SOURCE(Sketcher::Constraint, Base::Persistence) Constraint::Constraint() - : Value(0.0) - , Type(None) - , AlignmentType(Undef) - , First(GeoEnum::GeoUndef) - , FirstPos(PointPos::none) - , Second(GeoEnum::GeoUndef) - , SecondPos(PointPos::none) - , Third(GeoEnum::GeoUndef) - , ThirdPos(PointPos::none) - , LabelDistance(10.f) - , LabelPosition(0.f) - , isDriving(true) - , InternalAlignmentIndex(-1) - , isInVirtualSpace(false) - , isActive(true) { // Initialize a random number generator, to avoid Valgrind false positives. // The random number generator is not threadsafe so we guard it. See @@ -90,19 +82,24 @@ Constraint* Constraint::copy() const temp->Type = this->Type; temp->AlignmentType = this->AlignmentType; temp->Name = this->Name; - temp->First = this->First; - temp->FirstPos = this->FirstPos; - temp->Second = this->Second; - temp->SecondPos = this->SecondPos; - temp->Third = this->Third; - temp->ThirdPos = this->ThirdPos; temp->LabelDistance = this->LabelDistance; temp->LabelPosition = this->LabelPosition; temp->isDriving = this->isDriving; temp->InternalAlignmentIndex = this->InternalAlignmentIndex; temp->isInVirtualSpace = this->isInVirtualSpace; temp->isActive = this->isActive; + temp->elements = this->elements; // Do not copy tag, otherwise it is considered a clone, and a "rename" by the expression engine. + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + temp->First = this->First; + temp->FirstPos = this->FirstPos; + temp->Second = this->Second; + temp->SecondPos = this->SecondPos; + temp->Third = this->Third; + temp->ThirdPos = this->ThirdPos; +#endif + return temp; } @@ -160,19 +157,42 @@ void Constraint::Save(Writer& writer) const << "InternalAlignmentIndex=\"" << InternalAlignmentIndex << "\" "; } writer.Stream() << "Value=\"" << Value << "\" " - << "First=\"" << First << "\" " - << "FirstPos=\"" << (int)FirstPos << "\" " - << "Second=\"" << Second << "\" " - << "SecondPos=\"" << (int)SecondPos << "\" " - << "Third=\"" << Third << "\" " - << "ThirdPos=\"" << (int)ThirdPos << "\" " << "LabelDistance=\"" << LabelDistance << "\" " << "LabelPosition=\"" << LabelPosition << "\" " << "IsDriving=\"" << (int)isDriving << "\" " << "IsInVirtualSpace=\"" << (int)isInVirtualSpace << "\" " - << "IsActive=\"" << (int)isActive << "\" />" + << "IsActive=\"" << (int)isActive << "\" "; - << std::endl; + // Save elements + { + // Ensure backwards compatibility with old versions + writer.Stream() << "First=\"" << getElement(0).GeoId << "\" " + << "FirstPos=\"" << getElement(0).posIdAsInt() << "\" " + << "Second=\"" << getElement(1).GeoId << "\" " + << "SecondPos=\"" << getElement(1).posIdAsInt() << "\" " + << "Third=\"" << getElement(2).GeoId << "\" " + << "ThirdPos=\"" << getElement(2).posIdAsInt() << "\" "; +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + auto elements = std::views::iota(size_t {0}, this->elements.size()) + | std::views::transform([&](size_t i) { + return getElement(i); + }); +#endif + auto geoIds = elements | std::views::transform([](const GeoElementId& e) { + return e.GeoId; + }); + auto posIds = elements | std::views::transform([](const GeoElementId& e) { + return e.posIdAsInt(); + }); + + const std::string ids = fmt::format("{}", fmt::join(geoIds, " ")); + const std::string positions = fmt::format("{}", fmt::join(posIds, " ")); + + writer.Stream() << "ElementIds=\"" << ids << "\" " + << "ElementPositions=\"" << positions << "\" "; + } + + writer.Stream() << "/>\n"; } void Constraint::Restore(XMLReader& reader) @@ -181,10 +201,6 @@ void Constraint::Restore(XMLReader& reader) Name = reader.getAttribute("Name"); Type = reader.getAttribute("Type"); Value = reader.getAttribute("Value"); - First = reader.getAttribute("First"); - FirstPos = reader.getAttribute("FirstPos"); - Second = reader.getAttribute("Second"); - SecondPos = reader.getAttribute("SecondPos"); if (this->Type == InternalAlignment) { AlignmentType = reader.getAttribute("InternalAlignmentType"); @@ -197,12 +213,6 @@ void Constraint::Restore(XMLReader& reader) AlignmentType = Undef; } - // read the third geo group if present - if (reader.hasAttribute("Third")) { - Third = reader.getAttribute("Third"); - ThirdPos = reader.getAttribute("ThirdPos"); - } - // Read the distance a constraint label has been moved if (reader.hasAttribute("LabelDistance")) { LabelDistance = (float)reader.getAttribute("LabelDistance"); @@ -223,19 +233,90 @@ void Constraint::Restore(XMLReader& reader) if (reader.hasAttribute("IsActive")) { isActive = reader.getAttribute("IsActive"); } + + if (reader.hasAttribute("ElementIds") && reader.hasAttribute("ElementPositions")) { + auto splitAndClean = [](std::string_view input) { + const char delimiter = ' '; + + auto tokens = input | std::views::split(delimiter) + | std::views::transform([](auto&& subrange) { + // workaround due to lack of std::ranges::to in c++20 + std::string token; + auto size = std::ranges::distance(subrange); + token.reserve(size); + for (char c : subrange) { + token.push_back(c); + } + return token; + }) + | std::views::filter([](const std::string& s) { + return !s.empty(); + }); + + return std::vector(tokens.begin(), tokens.end()); + }; + + const std::string elementIds = reader.getAttribute("ElementIds"); + const std::string elementPositions = reader.getAttribute("ElementPositions"); + + const auto ids = splitAndClean(elementIds); + const auto positions = splitAndClean(elementPositions); + + if (ids.size() != positions.size()) { + throw Base::ParserError(fmt::format("ElementIds and ElementPositions do not match in " + "size. Got {} ids and {} positions.", + ids.size(), + positions.size())); + } + + elements.clear(); + for (size_t i = 0; i < std::min(ids.size(), positions.size()); ++i) { + const int geoId {std::stoi(ids[i])}; + const PointPos pos {static_cast(std::stoi(positions[i]))}; + addElement(GeoElementId(geoId, pos)); + } + } + + // Ensure we have at least 3 elements + while (getElementsSize() < 3) { + addElement(GeoElementId(GeoEnum::GeoUndef, PointPos::none)); + } + + // Load deprecated First, Second, Third elements + // These take precedence over the new elements + // Even though these are deprecated, we still need to read them + // for compatibility with old files. + { + constexpr std::array names = {"First", "Second", "Third"}; + constexpr std::array posNames = {"FirstPos", "SecondPos", "ThirdPos"}; + static_assert(names.size() == posNames.size()); + + for (size_t i = 0; i < names.size(); ++i) { + if (reader.hasAttribute(names[i])) { + const int geoId {reader.getAttribute(names[i])}; + const PointPos pos {reader.getAttribute(posNames[i])}; + setElement(i, GeoElementId(geoId, pos)); + } + } + } } void Constraint::substituteIndex(int fromGeoId, int toGeoId) { - if (this->First == fromGeoId) { - this->First = toGeoId; +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + for (size_t i = 0; i < elements.size(); ++i) { + const GeoElementId element = getElement(i); + if (element.GeoId == fromGeoId) { + setElement(i, GeoElementId(toGeoId, element.Pos)); + } } - if (this->Second == fromGeoId) { - this->Second = toGeoId; - } - if (this->Third == fromGeoId) { - this->Third = toGeoId; +#else + for (auto& element : elements) { + if (element.GeoId == fromGeoId) { + element = GeoElementId(toGeoId, element.Pos); + } } +#endif } void Constraint::substituteIndexAndPos(int fromGeoId, @@ -243,18 +324,22 @@ void Constraint::substituteIndexAndPos(int fromGeoId, int toGeoId, PointPos toPosId) { - if (this->First == fromGeoId && this->FirstPos == fromPosId) { - this->First = toGeoId; - this->FirstPos = toPosId; + const GeoElementId from {fromGeoId, fromPosId}; + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + for (size_t i = 0; i < elements.size(); ++i) { + const GeoElementId element = getElement(i); + if (element == from) { + setElement(i, GeoElementId(toGeoId, toPosId)); + } } - if (this->Second == fromGeoId && this->SecondPos == fromPosId) { - this->Second = toGeoId; - this->SecondPos = toPosId; - } - if (this->Third == fromGeoId && this->ThirdPos == fromPosId) { - this->Third = toGeoId; - this->ThirdPos = toPosId; +#else + for (auto& element : elements) { + if (element == from) { + element = GeoElementId(toGeoId, toPosId); + } } +#endif } std::string Constraint::typeToString(ConstraintType type) @@ -266,3 +351,91 @@ std::string Constraint::internalAlignmentTypeToString(InternalAlignmentType alig { return internalAlignmentType2str[alignment]; } + +bool Constraint::involvesGeoId(int geoId) const +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + auto elements = + std::views::iota(size_t {0}, this->elements.size()) | std::views::transform([&](size_t i) { + return getElement(i); + }); +#endif + return std::ranges::any_of(elements, [geoId](const auto& element) { + return element.GeoId == geoId; + }); +} +/// utility function to check if (`geoId`, `posId`) is one of the points/curves +bool Constraint::involvesGeoIdAndPosId(int geoId, PointPos posId) const +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + auto elements = + std::views::iota(size_t {0}, this->elements.size()) | std::views::transform([&](size_t i) { + return getElement(i); + }); +#endif + return std::ranges::find(elements, GeoElementId(geoId, posId)) != elements.end(); +} + +GeoElementId Constraint::getElement(size_t index) const +{ + if (index >= elements.size()) { + throw Base::IndexError("Constraint::getElement index out of range"); + } + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + return GeoElementId(First, FirstPos); + case 1: + return GeoElementId(Second, SecondPos); + case 2: + return GeoElementId(Third, ThirdPos); + } + } +#endif + return elements[index]; +} +void Constraint::setElement(size_t index, GeoElementId element) +{ + if (index >= elements.size()) { + throw Base::IndexError("Constraint::getElement index out of range"); + } + + elements[index] = element; + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + if (index < 3) { + switch (index) { + case 0: + First = element.GeoId; + FirstPos = element.Pos; + break; + case 1: + Second = element.GeoId; + SecondPos = element.Pos; + break; + case 2: + Third = element.GeoId; + ThirdPos = element.Pos; + break; + } + } +#endif +} + +size_t Constraint::getElementsSize() const +{ + return elements.size(); +} + +void Constraint::addElement(GeoElementId element) +{ +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + int i = elements.size(); + elements.resize(i + 1); + setElement(i, element); +#else + elements.push_back(element); +#endif +} diff --git a/src/Mod/Sketcher/App/Constraint.h b/src/Mod/Sketcher/App/Constraint.h index 4047a238a8..00491e5bd4 100644 --- a/src/Mod/Sketcher/App/Constraint.h +++ b/src/Mod/Sketcher/App/Constraint.h @@ -33,6 +33,11 @@ #include "GeoEnum.h" +// Flipping this to 0 removes old legazy members First, FirstPos, Second... +// Will be used when everything has been migrated to new api. +#define SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS 1 + + namespace Sketcher { /*! @@ -129,24 +134,18 @@ public: || Type == Diameter || Type == Angle || Type == SnellsLaw || Type == Weight; } - /// utility function to swap the index in First/Second/Third of the provided constraint from the + /// utility function to swap the index in elements of the provided constraint from the /// fromGeoId GeoId to toGeoId void substituteIndex(int fromGeoId, int toGeoId); - /// utility function to swap the index and position in First/Second/Third of the provided + /// utility function to swap the index and position in elements of the provided /// constraint from {fromGeoId, fromPosId} to {toGeoId, toPosId}. void substituteIndexAndPos(int fromGeoId, PointPos fromPosId, int toGeoId, PointPos toPosId); /// utility function to check if `geoId` is one of the geometries - bool involvesGeoId(int geoId) const - { - return First == geoId || Second == geoId || Third == geoId; - } + bool involvesGeoId(int geoId) const; + /// utility function to check if (`geoId`, `posId`) is one of the points/curves - bool involvesGeoIdAndPosId(int geoId, PointPos posId) const - { - return (First == geoId && FirstPos == posId) || (Second == geoId && SecondPos == posId) - || (Third == geoId && ThirdPos == posId); - } + bool involvesGeoIdAndPosId(int geoId, PointPos posId) const; std::string typeToString() const { @@ -166,7 +165,7 @@ private: Constraint(const Constraint&) = default; // only for internal use private: - double Value; + double Value {0.0}; // clang-format off constexpr static std::array type2str { @@ -207,24 +206,38 @@ private: "ParabolaFocalAxis"}}; public: - ConstraintType Type; - InternalAlignmentType AlignmentType; + ConstraintType Type {None}; + InternalAlignmentType AlignmentType {Undef}; std::string Name; - int First; - PointPos FirstPos; - int Second; - PointPos SecondPos; - int Third; - PointPos ThirdPos; - float LabelDistance; - float LabelPosition; - bool isDriving; + float LabelDistance {10.F}; + float LabelPosition {0.F}; + bool isDriving {true}; // Note: for InternalAlignment Type this index indexes equal internal geometry elements (e.g. // index of pole in a bspline). It is not a GeoId!! - int InternalAlignmentIndex; - bool isInVirtualSpace; + int InternalAlignmentIndex {-1}; + bool isInVirtualSpace {false}; - bool isActive; + bool isActive {true}; + + GeoElementId getElement(size_t index) const; + void setElement(size_t index, GeoElementId element); + size_t getElementsSize() const; + void addElement(GeoElementId element); + +#ifdef SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + // Deprecated, use getElement/setElement instead + int First {GeoEnum::GeoUndef}; + int Second {GeoEnum::GeoUndef}; + int Third {GeoEnum::GeoUndef}; + PointPos FirstPos {PointPos::none}; + PointPos SecondPos {PointPos::none}; + PointPos ThirdPos {PointPos::none}; +#endif + +private: + // New way to access point ids and positions. + // While the old way is still supported, it is recommended to the getters and setters instead. + std::vector elements {GeoElementId(), GeoElementId(), GeoElementId()}; protected: boost::uuids::uuid tag; diff --git a/src/Mod/Sketcher/App/PreCompiled.h b/src/Mod/Sketcher/App/PreCompiled.h index 5bdacf3b44..b51f4033d0 100644 --- a/src/Mod/Sketcher/App/PreCompiled.h +++ b/src/Mod/Sketcher/App/PreCompiled.h @@ -32,12 +32,15 @@ #ifdef _PreComp_ // standard +#include #include #include #include #include #include +#include #include +#include #include // Qt diff --git a/tests/src/Mod/Sketcher/App/CMakeLists.txt b/tests/src/Mod/Sketcher/App/CMakeLists.txt index 4f0e1dd60d..470ff7adfb 100644 --- a/tests/src/Mod/Sketcher/App/CMakeLists.txt +++ b/tests/src/Mod/Sketcher/App/CMakeLists.txt @@ -1,4 +1,5 @@ add_executable(Sketcher_tests_run + Constraint.cpp SketcherTestHelpers.cpp SketchObject.cpp SketchObjectChanges.cpp diff --git a/tests/src/Mod/Sketcher/App/Constraint.cpp b/tests/src/Mod/Sketcher/App/Constraint.cpp new file mode 100644 index 0000000000..c453dc87c3 --- /dev/null +++ b/tests/src/Mod/Sketcher/App/Constraint.cpp @@ -0,0 +1,571 @@ + +#include + +#include +#include +#include + +#include +#include +#include +#include + + +// Ensure Xerces is initialized before running tests which uses xml +class XercesEnvironment: public ::testing::Environment +{ +public: + void SetUp() override + { + try { + xercesc::XMLPlatformUtils::Initialize(); + } + catch (const xercesc::XMLException& e) { + FAIL() << "Xerces init failed: " << xercesc::XMLString::transcode(e.getMessage()); + } + } + + void TearDown() override + { + xercesc::XMLPlatformUtils::Terminate(); + } +}; + +::testing::Environment* const xercesEnv = + ::testing::AddGlobalTestEnvironment(new XercesEnvironment); + + +TEST(ConstraintPointsAccess, testDefaultGeoElementIdsAreSane) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act - no action needed, we are testing the default state + + // Assert +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS + // Old way of accessing elements + EXPECT_EQ(constraint.First, Sketcher::GeoEnum::GeoUndef); + EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::none); + + EXPECT_EQ(constraint.Second, Sketcher::GeoEnum::GeoUndef); + EXPECT_EQ(constraint.SecondPos, Sketcher::PointPos::none); + + EXPECT_EQ(constraint.Third, Sketcher::GeoEnum::GeoUndef); + EXPECT_EQ(constraint.ThirdPos, Sketcher::PointPos::none); + + // New way of accessing elements +#endif + EXPECT_EQ(constraint.getElement(0), + Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)); + EXPECT_EQ(constraint.getElement(1), + Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)); + EXPECT_EQ(constraint.getElement(2), + Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)); +} + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS +TEST(ConstraintPointsAccess, testOldWriteIsReadByNew) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.First = 23; + constraint.FirstPos = Sketcher::PointPos::start; + constraint.Second = 34; + constraint.SecondPos = Sketcher::PointPos::end; + constraint.Third = 45; + constraint.ThirdPos = Sketcher::PointPos::mid; + + // Assert + EXPECT_EQ(constraint.getElement(0), + Sketcher::GeoElementId(Sketcher::GeoElementId(23, Sketcher::PointPos::start))); + EXPECT_EQ(constraint.getElement(1), + Sketcher::GeoElementId(Sketcher::GeoElementId(34, Sketcher::PointPos::end))); + EXPECT_EQ(constraint.getElement(2), + Sketcher::GeoElementId(Sketcher::GeoElementId(45, Sketcher::PointPos::mid))); +} + +TEST(ConstraintPointsAccess, testNewWriteIsReadByOld) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid)); + + // Assert + EXPECT_EQ(constraint.First, 23); + EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::start); + EXPECT_EQ(constraint.Second, 34); + EXPECT_EQ(constraint.SecondPos, Sketcher::PointPos::end); + EXPECT_EQ(constraint.Third, 45); + EXPECT_EQ(constraint.ThirdPos, Sketcher::PointPos::mid); +} +#endif + +TEST(ConstraintPointsAccess, testThreeElementsByDefault) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act - no action needed, we are testing the default state + + // Assert + EXPECT_EQ(constraint.getElementsSize(), 3); +} + +TEST(ConstraintPointsAccess, testFourElementsWhenAddingOne) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.addElement(Sketcher::GeoElementId(1, Sketcher::PointPos::start)); + + // Assert + EXPECT_EQ(constraint.getElementsSize(), 4); +} + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS +TEST(ConstraintPointsAccess, testElementSerializationWhenAccessingOldWay) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.First = 23; + constraint.FirstPos = Sketcher::PointPos::start; + constraint.Second = 34; + constraint.SecondPos = Sketcher::PointPos::end; + constraint.Third = 45; + constraint.ThirdPos = Sketcher::PointPos::mid; + + Base::StringWriter writer = {}; + constraint.Save(writer); + + // Assert + std::string serialized = writer.getString(); + EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos); + EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos); + EXPECT_TRUE(serialized.find("Second=\"34\"") != std::string::npos); + EXPECT_TRUE(serialized.find("SecondPos=\"2\"") != std::string::npos); + EXPECT_TRUE(serialized.find("Third=\"45\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ThirdPos=\"3\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ElementIds=\"23 34 45\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ElementPositions=\"1 2 3\"") != std::string::npos); +} +#endif + +TEST(ConstraintPointsAccess, testElementSerializationWhenAccessingNewWay) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid)); + + Base::StringWriter writer = {}; + constraint.Save(writer); + + // Assert + std::string serialized = writer.getString(); + EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos); + EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos); + EXPECT_TRUE(serialized.find("Second=\"34\"") != std::string::npos); + EXPECT_TRUE(serialized.find("SecondPos=\"2\"") != std::string::npos); + EXPECT_TRUE(serialized.find("Third=\"45\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ThirdPos=\"3\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ElementIds=\"23 34 45\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ElementPositions=\"1 2 3\"") != std::string::npos); +} + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS +TEST(ConstraintPointsAccess, testElementSerializationWhenMixingOldAndNew) // NOLINT +{ + // Arrange + auto constraint = Sketcher::Constraint(); + + // Act + constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + constraint.Second = 45; // Old way + constraint.SecondPos = Sketcher::PointPos::mid; + + Base::StringWriter writer = {}; + constraint.Save(writer); + + // Assert + std::string serialized = writer.getString(); + EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos); + EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos); + + // Old way wrote this data + // ensure mid is 3 for next test + EXPECT_EQ(Sketcher::PointPos::mid, static_cast(3)); + EXPECT_TRUE(serialized.find("SecondPos=\"3\"") != std::string::npos); + EXPECT_TRUE(serialized.find("Second=\"45\"") != std::string::npos); + + EXPECT_TRUE(serialized.find("Third=\"-2000\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ThirdPos=\"0\"") != std::string::npos); + + // Second and SecondPos is reflected in the elements data too + EXPECT_TRUE(serialized.find("ElementIds=\"23 45 -2000\"") != std::string::npos); + EXPECT_TRUE(serialized.find("ElementPositions=\"1 3 0\"") != std::string::npos); +} +#endif + +TEST(ConstraintPointsAccess, testElementsRestoredFromSerialization) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid)); + + Base::StringWriter writer; + writer.Stream() << "\n"; // Wrap in a root element to make constraint.Save happy + constraint.Save(writer); + writer.Stream() << ""; + + // Write to temporary file + QTemporaryFile tempFile; + tempFile.setAutoRemove(true); + ASSERT_TRUE(tempFile.open()); + tempFile.write(writer.getString().c_str(), writer.getString().size()); + tempFile.flush(); + + // Open with std::ifstream and parse + std::string filename = tempFile.fileName().toStdString(); + std::ifstream inputFile(filename); + ASSERT_TRUE(inputFile.is_open()); + + Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile); + Sketcher::Constraint restoredConstraint; + restoredConstraint.Restore(reader); + + // Assert + EXPECT_EQ(restoredConstraint.getElement(0), + Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + EXPECT_EQ(restoredConstraint.getElement(1), + Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + EXPECT_EQ(restoredConstraint.getElement(2), + Sketcher::GeoElementId(45, Sketcher::PointPos::mid)); + + inputFile.close(); +} + +TEST(ConstraintPointsAccess, + testElementsRestoredFromSerializationWithoutNewElementStorage) // NOLINT +{ + // Arrange + + // Manually craft a serialized version, only parts in "{}" are important. + // New way of storing elements is not present, like if it is an older file. + std::string serializedConstraint = fmt::format("", + + 67, + 78, + 89, + static_cast(Sketcher::PointPos::mid), + static_cast(Sketcher::PointPos::start), + static_cast(Sketcher::PointPos::end)); + + Base::StringWriter writer; + auto& stream {writer.Stream()}; + stream << "\n"; // Wrap in a root element to make constraint. + stream << serializedConstraint; + stream << ""; + + // Write to temporary file + QTemporaryFile tempFile; + tempFile.setAutoRemove(true); + ASSERT_TRUE(tempFile.open()); + tempFile.write(writer.getString().c_str(), writer.getString().size()); + tempFile.flush(); + + // Open with std::ifstream and parse + std::string filename = tempFile.fileName().toStdString(); + std::ifstream inputFile(filename); + ASSERT_TRUE(inputFile.is_open()); + + Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile); + Sketcher::Constraint restoredConstraint; + restoredConstraint.Restore(reader); + + // Assert + EXPECT_EQ(restoredConstraint.getElement(0), + Sketcher::GeoElementId(67, Sketcher::PointPos::mid)); + EXPECT_EQ(restoredConstraint.getElement(1), + Sketcher::GeoElementId(78, Sketcher::PointPos::start)); + EXPECT_EQ(restoredConstraint.getElement(2), + Sketcher::GeoElementId(89, Sketcher::PointPos::end)); + + inputFile.close(); +} + +TEST(ConstraintPointsAccess, + testLegacyIsPreferedDuringSerializationWithoutLegacyElementStorage) // NOLINT +{ + // Arrange + + // Manually craft a serialized version, only parts in "{}" are important. + // Only new way of storing elements is present. + std::string serializedConstraint = fmt::format("", + // New way data + 23, + 34, + 45, + static_cast(Sketcher::PointPos::start), + static_cast(Sketcher::PointPos::end), + static_cast(Sketcher::PointPos::mid)); + + Base::StringWriter writer; + auto& stream {writer.Stream()}; + stream << "\n"; // Wrap in a root element to make constraint. + stream << serializedConstraint; + stream << ""; + + // Write to temporary file + QTemporaryFile tempFile; + tempFile.setAutoRemove(true); + ASSERT_TRUE(tempFile.open()); + tempFile.write(writer.getString().c_str(), writer.getString().size()); + tempFile.flush(); + + // Open with std::ifstream and parse + std::string filename = tempFile.fileName().toStdString(); + std::ifstream inputFile(filename); + ASSERT_TRUE(inputFile.is_open()); + + Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile); + Sketcher::Constraint restoredConstraint; + restoredConstraint.Restore(reader); + + // Assert + EXPECT_EQ(restoredConstraint.getElement(0), + Sketcher::GeoElementId(23, Sketcher::PointPos::start)); + EXPECT_EQ(restoredConstraint.getElement(1), + Sketcher::GeoElementId(34, Sketcher::PointPos::end)); + EXPECT_EQ(restoredConstraint.getElement(2), + Sketcher::GeoElementId(45, Sketcher::PointPos::mid)); + + inputFile.close(); +} + +TEST(ConstraintPointsAccess, testLegacyIsPreferedDuringSerializationIfContradicting) // NOLINT +{ + // Arrange + + // Manually craft a serialized version, only parts in "{}" are important. + // It is not important if legacy is included before or after, legacy should always be preferred. + std::string serializedConstraint = + fmt::format("", + // New way data + 23, + 34, + 45, + static_cast(Sketcher::PointPos::start), + static_cast(Sketcher::PointPos::end), + static_cast(Sketcher::PointPos::mid), + + // Contradicting legacy data, this should be preferred if available + 67, + 78, + 89, + static_cast(Sketcher::PointPos::mid), + static_cast(Sketcher::PointPos::start), + static_cast(Sketcher::PointPos::end)); + + Base::StringWriter writer; + auto& stream {writer.Stream()}; + stream << "\n"; // Wrap in a root element to make constraint. + stream << serializedConstraint; + stream << ""; + + // Write to temporary file + QTemporaryFile tempFile; + tempFile.setAutoRemove(true); + ASSERT_TRUE(tempFile.open()); + tempFile.write(writer.getString().c_str(), writer.getString().size()); + tempFile.flush(); + + // Open with std::ifstream and parse + std::string filename = tempFile.fileName().toStdString(); + std::ifstream inputFile(filename); + ASSERT_TRUE(inputFile.is_open()); + + Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile); + Sketcher::Constraint restoredConstraint; + restoredConstraint.Restore(reader); + + // Assert + EXPECT_EQ(restoredConstraint.getElement(0), + Sketcher::GeoElementId(67, Sketcher::PointPos::mid)); + EXPECT_EQ(restoredConstraint.getElement(1), + Sketcher::GeoElementId(78, Sketcher::PointPos::start)); + EXPECT_EQ(restoredConstraint.getElement(2), + Sketcher::GeoElementId(89, Sketcher::PointPos::end)); + + inputFile.close(); +} + +TEST(ConstraintPointsAccess, testSubstituteIndex) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::end)); + constraint.setElement(2, + Sketcher::GeoElementId(10, Sketcher::PointPos::mid)); // same GeoId as 0 + + // Act + constraint.substituteIndex(10, 99); + + // Assert + EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(99, Sketcher::PointPos::start)); + EXPECT_EQ(constraint.getElement(1), Sketcher::GeoElementId(20, Sketcher::PointPos::end)); + EXPECT_EQ(constraint.getElement(2), Sketcher::GeoElementId(99, Sketcher::PointPos::mid)); +} + +TEST(ConstraintPointsAccess, testSubstituteIndexAndPos) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::start)); + constraint.setElement(2, Sketcher::GeoElementId(10, Sketcher::PointPos::mid)); + + // Act + constraint.substituteIndexAndPos(10, Sketcher::PointPos::start, 42, Sketcher::PointPos::end); + + // Assert + EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(42, Sketcher::PointPos::end)); + EXPECT_EQ(constraint.getElement(1), Sketcher::GeoElementId(20, Sketcher::PointPos::start)); + EXPECT_EQ(constraint.getElement(2), + Sketcher::GeoElementId(10, Sketcher::PointPos::mid)); // unchanged +} + +TEST(ConstraintPointsAccess, testInvolvesGeoId) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::end)); + + // Act & Assert + EXPECT_TRUE(constraint.involvesGeoId(10)); + EXPECT_TRUE(constraint.involvesGeoId(20)); + EXPECT_FALSE(constraint.involvesGeoId(99)); +} + +TEST(ConstraintPointsAccess, testInvolvesGeoIdAndPosId) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start)); + constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::mid)); + constraint.setElement(2, Sketcher::GeoElementId(30, Sketcher::PointPos::end)); + + // Act & Assert + EXPECT_TRUE(constraint.involvesGeoIdAndPosId(10, Sketcher::PointPos::start)); + EXPECT_TRUE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::mid)); + EXPECT_FALSE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::start)); + EXPECT_FALSE(constraint.involvesGeoIdAndPosId(99, Sketcher::PointPos::end)); +} + +#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS +TEST(ConstraintPointsAccess, testLegacyWriteReflectedInInvolvesAndSubstitute) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.First = 10; + constraint.FirstPos = Sketcher::PointPos::start; + constraint.Second = 20; + constraint.SecondPos = Sketcher::PointPos::end; + + // Act & Assert + EXPECT_TRUE(constraint.involvesGeoId(10)); + EXPECT_TRUE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::end)); + + // Substitute the legacy-indexed element + constraint.substituteIndex(10, 99); + + // Should now reflect the substituted value + EXPECT_TRUE(constraint.involvesGeoId(99)); + EXPECT_FALSE(constraint.involvesGeoId(10)); +} + +TEST(ConstraintPointsAccess, testSubstituteUpdatesLegacyFieldsToo) // NOLINT +{ + // Arrange + Sketcher::Constraint constraint; + constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start)); + + // Act + constraint.substituteIndex(10, 42); + + // Assert + EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(42, Sketcher::PointPos::start)); + EXPECT_EQ(constraint.First, 42); + EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::start); +} +#endif