// SPDX-License-Identifier: LGPL-2.1-or-later #include #include #include #include #include #include #include #include #include #include "SketcherTestHelpers.h" using namespace SketcherTestHelpers; TEST_F(SketchObjectTest, testAddExternalIncreasesCount) { // Arrange auto* doc = getObject()->getDocument(); auto box {doc->addObject("Part::Box")}; int numboxes = doc->countObjectsOfType(); int numExtPre = getObject()->ExternalGeo.getSize(); doc->recompute(); // Act getObject()->addExternal(box, "Face6"); int numExt = getObject()->ExternalGeo.getSize(); // Assert EXPECT_TRUE(numExt > numExtPre); } TEST_F(SketchObjectTest, testDelExternalUndef) { // Act int res = getObject()->delExternal(Sketcher::GeoEnum::GeoUndef); // Assert EXPECT_EQ(res, -1); } TEST_F(SketchObjectTest, testDelExternalWhenEmpty) { // Act int res = getObject()->delExternal(Sketcher::GeoEnum::RefExt); // Assert EXPECT_EQ(res, -1); } TEST_F(SketchObjectTest, testDelExternalWhenEmptyWithPositiveId) { // Act int res = getObject()->delExternal(1); // Assert EXPECT_EQ(res, -1); } TEST_F(SketchObjectTest, testDelExternalReducesCount) { // Arrange auto* doc = getObject()->getDocument(); auto box {doc->addObject("Part::Box")}; doc->recompute(); // NOTE: When adding, say, `Face6` instead, `delExternal` removes all the edges. This could be // intended behaviour, ensuring that adding a face always gives a closed loop, or a side effect, // or should instead be an option at time of external geometry creation. // TODO: Consider whether this should be intended behaviour, and add tests accordingly. getObject()->addExternal(box, "Edge6"); getObject()->addExternal(box, "Edge4"); int numExt = getObject()->ExternalGeo.getSize(); // Act int res = getObject()->delExternal(Sketcher::GeoEnum::RefExt); // Assert EXPECT_EQ(getObject()->ExternalGeo.getSize(), numExt - 1); } // TODO: `delExternal` situation of constraints // TODO: `delExternal` situation of constraint containing more than 3 entities // TODO: `addCopy` tests // TODO: ensure new item(s) is/are added and of same type // TODO: behaviour of `addCopy` when external // TODO: constraints on new copies? // TODO: when empty list is passed TEST_F(SketchObjectTest, testReplaceGeometriesOneToOne) { // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); int geoId = getObject()->addGeometry(&lineSeg); std::vector newCurves {createTypicalNonPeriodicBSpline().release()}; // Act getObject()->replaceGeometries({geoId}, newCurves); // Assert // Ensure geoId1 is now a B-Spline auto* geo = getObject()->getGeometry(geoId); EXPECT_TRUE(geo->is()); } TEST_F(SketchObjectTest, testReplaceGeometriesTwoToOne) { // Arrange Part::GeomLineSegment lineSeg1, lineSeg2; setupLineSegment(lineSeg1); int geoId1 = getObject()->addGeometry(&lineSeg1); setupLineSegment(lineSeg2); int geoId2 = getObject()->addGeometry(&lineSeg2); std::vector newCurves {createTypicalNonPeriodicBSpline().release()}; // Act getObject()->replaceGeometries({geoId1, geoId2}, newCurves); // Assert // Ensure only one curve EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); // Ensure geoId1 is now a B-Spline auto* geo = getObject()->getGeometry(geoId1); EXPECT_TRUE(geo->is()); } TEST_F(SketchObjectTest, testReplaceGeometriesOneToTwo) { // Arrange Part::GeomLineSegment lineSeg1; setupLineSegment(lineSeg1); int geoId1 = getObject()->addGeometry(&lineSeg1); std::vector newCurves { createTypicalNonPeriodicBSpline().release(), createTypicalNonPeriodicBSpline().release() }; // Act getObject()->replaceGeometries({geoId1}, newCurves); // Assert // Ensure only one curve EXPECT_EQ(getObject()->getHighestCurveIndex(), 1); // Ensure geoId1 is now a B-Spline auto* geo = getObject()->getGeometry(geoId1); EXPECT_TRUE(geo->is()); geo = getObject()->getGeometry(geoId1 + 1); EXPECT_TRUE(geo->is()); } // TODO: formulate and add any constraint related changes when replacing geometries // Currently, `replageGeometries` is a very "low level" operation that directly replaces the // elements of the vector of geometries. Constraint handling is intended to be done by the // operations that call this method. We may want to add tests that ensure constraints aren't // touched. Alternatively, we may want to change `replaceGeometries` such that it modifies // constraints, though that will limit our control in individual operations. TEST_F(SketchObjectTest, testSplitLineSegment) { // Arrange Base::Vector3d splitPoint(2.0, 3.1, 0.0); Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); int geoId = getObject()->addGeometry(&lineSeg); // Act int result = getObject()->split(geoId, splitPoint); // Assert EXPECT_EQ(result, 0); // One additional curve should be added EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId + 1); // Expect the resultant curves are line segments and shape is conserved int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testSplitCircle) { // Arrange Base::Vector3d splitPoint(2.0, 3.1, 0.0); Part::GeomCircle circle; setupCircle(circle); int geoId = getObject()->addGeometry(&circle); // Act int result = getObject()->split(geoId, splitPoint); // Assert EXPECT_EQ(result, 0); // The circle should be split into an arc now EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId); } TEST_F(SketchObjectTest, testSplitEllipse) { // Arrange Base::Vector3d splitPoint(2.0, 3.1, 0.0); Part::GeomEllipse ellipse; setupEllipse(ellipse); int geoId = getObject()->addGeometry(&ellipse); // Act int result = getObject()->split(geoId, splitPoint); // Assert EXPECT_EQ(result, 0); // The ellipse should be split into an arc of ellipse now // FIXME: Internal geometries may be added or removed which may cause some issues // EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId); } TEST_F(SketchObjectTest, testSplitArcOfCircle) { // Arrange Base::Vector3d splitPoint(-2.0, 3.1, 0.0); Part::GeomArcOfCircle arcOfCircle; setupArcOfCircle(arcOfCircle); int geoId = getObject()->addGeometry(&arcOfCircle); // Act int result = getObject()->split(geoId, splitPoint); // Assert EXPECT_EQ(result, 0); // The arcOfCircle should be split into an arc now EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId + 1); // Expect the end points and centers of the resultant curve are coincident. int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 2); } TEST_F(SketchObjectTest, testSplitArcOfConic) { // Arrange // Define a parabola/hyperbola as reference Base::Vector3d splitPoint(1.0, -1.1, 0.0); Part::GeomArcOfParabola arcOfConic; setupArcOfParabola(arcOfConic); int geoId = getObject()->addGeometry(&arcOfConic); // Act // TODO: Sample random points from both sides of the split int result = getObject()->split(geoId, splitPoint); for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); // The arcOfConic should be split into two arcs of the same conic now EXPECT_EQ(getObject()->getHighestCurveIndex(), 1); // Expect the end points of the resultant curve are coincident. int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testSplitNonPeriodicBSpline) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); Base::Vector3d splitPoint(-0.5, 1.1, 0.0); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); // TODO: Put a point on this // Act // TODO: sample before point(s) at a random parameter int result = getObject()->split(geoId, splitPoint); for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); EXPECT_EQ(getObject()->getHighestCurveIndex(), 1); // TODO: confirm sampled point(s) is/are at the same place int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testSplitPeriodicBSpline) { // Arrange auto PeriodicBSpline = createTypicalPeriodicBSpline(); Base::Vector3d splitPoint(-0.5, 1.1, 0.0); int geoId = getObject()->addGeometry(PeriodicBSpline.get()); // TODO: Put a point on this // Act // TODO: sample before point(s) at a random parameter int result = getObject()->split(geoId, splitPoint); for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); // TODO: confirm sampled point(s) is/are at the same place } TEST_F(SketchObjectTest, testTrimWithoutIntersection) { // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); int geoId = getObject()->addGeometry(&lineSeg); Base::Vector3d trimPoint(2.0, 3.1, 0.0); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // Once this line segment is trimmed, nothing should remain EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId - 1); } // TODO: There are other combinations of constraints we may want to test with trim. TEST_F(SketchObjectTest, testTrimLineSegmentEnd) { // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(lineSeg, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(lineSeg, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&lineSeg); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // TODO: Once this line segment is trimmed, the curve should be "smaller" EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId); // TODO: There should be a "point-on-object" constraint on the intersecting curves int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testTrimLineSegmentMid) { // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); // TODO: create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(lineSeg, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(lineSeg, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(lineSeg, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y - 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y += 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(&lineSeg); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // TODO: Once this line segment is trimmed, there should be two "smaller" curves in its place EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId + 1); // TODO: There should be a "point-on-object" constraint on the intersecting curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); // TODO: Ensure shape is preserved } TEST_F(SketchObjectTest, testTrimCircleEnd) { // Arrange Part::GeomCircle circle; setupCircle(circle); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(circle, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(circle, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&circle); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // TODO: Once this circle is trimmed, the circle should be deleted. EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId - 1); } TEST_F(SketchObjectTest, testTrimCircleMid) { // Arrange Part::GeomCircle circle; setupCircle(circle); // TODO: create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(circle, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(circle, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(circle, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(&circle); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // TODO: Once this circle is trimmed, there should be one arc. EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId); // There should be one "coincident" and one "point-on-object" constraint on the intersecting // curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); // TODO: Ensure shape is preserved } TEST_F(SketchObjectTest, testTrimArcOfCircleEnd) { // This should also cover as a representative of arc of conic // Arrange Part::GeomArcOfCircle arcOfCircle; setupArcOfCircle(arcOfCircle); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(arcOfCircle, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(arcOfCircle, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&arcOfCircle); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId); // There should be a "point-on-object" constraint on the intersecting curves int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testTrimArcOfCircleMid) { // Arrange Part::GeomArcOfCircle arcOfCircle; setupArcOfCircle(arcOfCircle); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(arcOfCircle, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(arcOfCircle, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(arcOfCircle, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(&arcOfCircle); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); EXPECT_EQ(getObject()->getHighestCurveIndex(), geoId + 1); // There should be a "point-on-object" constraint on the intersecting curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); // There should be 2 coincident constraints: one with lineSegCut1 and one between centers of the // new arcs int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 2); // TODO: Ensure shape is preserved } TEST_F(SketchObjectTest, testTrimEllipseEnd) { // Arrange Part::GeomEllipse ellipse; setupEllipse(ellipse); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(ellipse, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(ellipse, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&ellipse); // Act int result = getObject()->trim(geoId, trimPoint); // remove all internal geometry for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); // Once this ellipse is trimmed, the ellipse should be deleted. // Only remaining: line segment EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); } TEST_F(SketchObjectTest, testTrimEllipseMid) { // Arrange Part::GeomEllipse ellipse; setupEllipse(ellipse); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(ellipse, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(ellipse, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(ellipse, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(&ellipse); // FIXME: Doing this to avoid trimming only until minor/major axes. Should not be needed. getObject()->deleteUnusedInternalGeometry(geoId); // Act int result = getObject()->trim(geoId, trimPoint); // remove all internal geometry for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); // Once this ellipse is trimmed, there should be one arc and line segments. EXPECT_EQ(getObject()->getHighestCurveIndex(), 2); // There should be one "coincident" and one "point-on-object" constraint on the intersecting // curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); // TODO: Ensure shape is preserved } // TODO: Tests for other arcs of conics? TEST_F(SketchObjectTest, testTrimPeriodicBSplineEnd) { // Arrange auto periodicBSpline = createTypicalPeriodicBSpline(); assert(periodicBSpline); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(*periodicBSpline, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(*periodicBSpline, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(periodicBSpline.get()); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // FIXME: This will fail because of deleted internal geometry // Once this periodicBSpline is trimmed, the periodicBSpline should be deleted, leaving only the // line segment. EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); // TODO: There should be a "point-on-object" constraint on the intersecting curves } TEST_F(SketchObjectTest, testTrimPeriodicBSplineMid) { // Arrange auto periodicBSpline = createTypicalPeriodicBSpline(); assert(periodicBSpline); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(*periodicBSpline, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(*periodicBSpline, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(*periodicBSpline, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(periodicBSpline.get()); // Act int result = getObject()->trim(geoId, trimPoint); // remove all internal geometry for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); // Only remaining: Two line segments and the B-spline EXPECT_EQ(getObject()->getHighestCurveIndex(), 2); // There should be one "coincident" and one "point-on-object" constraint on the intersecting // curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); // TODO: Ensure shape is preserved } TEST_F(SketchObjectTest, testTrimNonPeriodicBSplineEnd) { // This should also cover as a representative of arc of conic // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(*nonPeriodicBSpline, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(*nonPeriodicBSpline, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); // Act int result = getObject()->trim(geoId, trimPoint); // remove all internal geometry for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } // Assert EXPECT_EQ(result, 0); // Only remaining: one line segment and the trimmed B-spline EXPECT_EQ(getObject()->getHighestCurveIndex(), 1); // FIXME: There should be a "point-on-object" constraint on the intersecting curves int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); } TEST_F(SketchObjectTest, testTrimNonPeriodicBSplineMid) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(*nonPeriodicBSpline, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(*nonPeriodicBSpline, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(*nonPeriodicBSpline, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); // Act int result = getObject()->trim(geoId, trimPoint); // remove all internal geometry for (int i = 0; i < getObject()->getHighestCurveIndex(); ++i) { if (getObject()->getGeometry(i)->is()) { getObject()->deleteUnusedInternalGeometry(i); } } // Assert EXPECT_EQ(result, 0); // Only remaining: one line segment and the trimmed B-spline EXPECT_EQ(getObject()->getHighestCurveIndex(), 3); // There should be a "point-on-object" constraint on the intersecting curves int numberOfPointOnObjectConstraints = countConstraintsOfType(getObject(), Sketcher::PointOnObject); EXPECT_EQ(numberOfPointOnObjectConstraints, 1); int numberOfCoincidentConstraints = countConstraintsOfType(getObject(), Sketcher::Coincident); EXPECT_EQ(numberOfCoincidentConstraints, 1); // TODO: Ensure shape is preserved } TEST_F(SketchObjectTest, testTrimEffectOnConstruction) { // Ensure construction geometries remain construction // The issue #12715 was usually happening when the geometry changed type, which // currently only happens for circles and ellipses. // Arrange Part::GeomCircle circle; setupCircle(circle); // TODO: create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(circle, 0.5)); Base::Vector3d p1(getPointAtNormalizedParameter(circle, 0.3)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); Base::Vector3d p3(getPointAtNormalizedParameter(circle, 0.7)); Base::Vector3d p4(p3.x + 0.1, p3.y + 0.1, p3.z); // to ensure that this line clearly intersects the curve, not just have a point on object // without explicit constraint p3.x -= 0.1; p3.y -= 0.1; Part::GeomLineSegment lineSegCut2; lineSegCut2.setPoints(p3, p4); getObject()->addGeometry(&lineSegCut2); int geoId = getObject()->addGeometry(&circle, true); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); for (int i = 0; i < getObject()->getHighestCurveIndex(); ++i) { auto* geom = getObject()->getGeometry(i); if (geom->is()) { EXPECT_TRUE(GeometryFacade::getConstruction(geom)); } } } TEST_F(SketchObjectTest, testTrimEndEffectOnFullLengthConstraints) { // Ensure constraints directly applying to full length disappear if one of the ends disappears. // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); // TODO: create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(lineSeg, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(lineSeg, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&lineSeg); auto constr = new Sketcher::Constraint(); // Ownership will be transferred to the sketch constr->Type = Sketcher::ConstraintType::Distance; constr->First = geoId; constr->FirstPos = Sketcher::PointPos::none; constr->setValue((getObject()->getPoint(geoId, Sketcher::PointPos::end) - getObject()->getPoint(geoId, Sketcher::PointPos::start)) .Length()); getObject()->addConstraint(constr); // Assert EXPECT_EQ(countConstraintsOfType(getObject(), Sketcher::ConstraintType::Distance), 1); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); EXPECT_EQ(countConstraintsOfType(getObject(), Sketcher::ConstraintType::Distance), 0); } TEST_F(SketchObjectTest, testTrimEndEffectOnSymmetricConstraints) { // Ensure symmetric constraints go away // Arrange Part::GeomLineSegment lineSeg; setupLineSegment(lineSeg); // create curves intersecting at the right spots Base::Vector3d trimPoint(getPointAtNormalizedParameter(lineSeg, 0.2)); Base::Vector3d p1(getPointAtNormalizedParameter(lineSeg, 0.5)); Base::Vector3d p2(p1.x + 0.1, p1.y + 0.1, p1.z); Part::GeomLineSegment lineSegCut1; lineSegCut1.setPoints(p1, p2); int geoIdOfCutting = getObject()->addGeometry(&lineSegCut1); int geoId = getObject()->addGeometry(&lineSeg); auto constr = new Sketcher::Constraint(); // Ownership will be transferred to the sketch constr->Type = Sketcher::ConstraintType::Symmetric; constr->First = geoId; constr->FirstPos = Sketcher::PointPos::start; constr->Second = geoId; constr->SecondPos = Sketcher::PointPos::end; constr->Third = geoIdOfCutting; constr->ThirdPos = Sketcher::PointPos::start; getObject()->addConstraint(constr); // Assert EXPECT_EQ(countConstraintsOfType(getObject(), Sketcher::ConstraintType::Symmetric), 1); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); EXPECT_EQ(countConstraintsOfType(getObject(), Sketcher::ConstraintType::Symmetric), 0); } TEST_F(SketchObjectTest, testTrimEndEffectOnUnrelatedTangent) { // See https://github.com/AstoCAD/FreeCAD/issues/24 // Arrange Part::GeomLineSegment lineSeg; lineSeg.setPoints(Base::Vector3d {1.0, -2.0, 0.0}, Base::Vector3d {1.0, 2.0, 0.0}); Base::Vector3d trimPoint(getPointAtNormalizedParameter(lineSeg, 0.2)); int geoId = getObject()->addGeometry(&lineSeg); Part::GeomCircle innerCircle; innerCircle.setCenter(Base::Vector3d {0.0, 0.0, 0.0}); innerCircle.setRadius(1.0); int geoIdInnerCircle = getObject()->addGeometry(&innerCircle); Part::GeomCircle outerCircle; outerCircle.setCenter(Base::Vector3d {0.0, 0.0, 0.0}); outerCircle.setRadius(1.5); getObject()->addGeometry(&outerCircle); // no need to save // TODO: add tangent and confirm auto constraint = new Sketcher::Constraint(); // Ownership will be transferred to the sketch constraint->Type = Sketcher::ConstraintType::Tangent; constraint->First = geoId; constraint->FirstPos = Sketcher::PointPos::none; constraint->Second = geoIdInnerCircle; constraint->SecondPos = Sketcher::PointPos::none; getObject()->addConstraint(constraint); EXPECT_EQ(countConstraintsOfType(getObject(), Sketcher::ConstraintType::Tangent), 1); // Act int result = getObject()->trim(geoId, trimPoint); // Assert EXPECT_EQ(result, 0); // TODO: find tangent and confirm nature const auto& constraints = getObject()->Constraints.getValues(); auto tangIt = std::ranges::find( constraints, Sketcher::ConstraintType::Tangent, &Sketcher::Constraint::Type ); EXPECT_NE(tangIt, constraints.end()); EXPECT_EQ((*tangIt)->FirstPos, Sketcher::PointPos::none); EXPECT_EQ((*tangIt)->SecondPos, Sketcher::PointPos::none); } // TODO: Ensure endpoint constraints go to the appropriate new geometry // This will need a reliable way to get the resultant curves after the trim TEST_F(SketchObjectTest, testModifyKnotMultInNonPeriodicBSplineToZero) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); auto bsp1 = static_cast(getObject()->getGeometry(geoId)); int oldKnotCount = bsp1->countKnots(); // Act // Try decreasing mult to zero. // NOTE: we still use OCCT notation of knot index starting with 1 (not 0). getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -1); // Assert // Knot should disappear. We start with 3 (unique) knots, so expect 2. auto bsp2 = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp2->countKnots(), oldKnotCount - 1); } TEST_F(SketchObjectTest, testModifyKnotMultInNonPeriodicBSplineToDisallowed) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); // Act and Assert // TODO: Try modifying such that resultant multiplicity > degree // TODO: This should immediately throw exception EXPECT_THROW(getObject()->modifyBSplineKnotMultiplicity(geoId, 2, 3), Base::ValueError); // TODO: Try modifying such that resultant multiplicity < 0 // TODO: This should immediately throw exception EXPECT_THROW(getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -2), Base::ValueError); } TEST_F(SketchObjectTest, testModifyKnotMultInNonPeriodicBSpline) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); auto bsp = static_cast(getObject()->getGeometry(geoId)); int oldKnotsNum = bsp->countKnots(); int oldMultiplicityOfTargetKnot = bsp->getMultiplicities()[1]; // Act // TODO: Increase/decrease knot multiplicity normally getObject()->modifyBSplineKnotMultiplicity(geoId, 2, 1); // Assert // This should not alter the sizes of knot and multiplicity vectors. bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum); // This should increment the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[1], oldMultiplicityOfTargetKnot + 1); // This should still be a non-periodic spline EXPECT_FALSE(bsp->isPeriodic()); // TODO: Expect shape is preserved // Act // TODO: Increase/decrease knot multiplicity normally getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -1); // Assert // This should not alter the sizes of knot and multiplicity vectors. bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum); // This should increment the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[1], oldMultiplicityOfTargetKnot); // This should still be a non-periodic spline EXPECT_FALSE(bsp->isPeriodic()); } TEST_F(SketchObjectTest, testModifyKnotMultInPeriodicBSplineToZero) { // Arrange auto PeriodicBSpline = createTypicalPeriodicBSpline(); assert(PeriodicBSpline); int geoId = getObject()->addGeometry(PeriodicBSpline.get()); auto bsp1 = static_cast(getObject()->getGeometry(geoId)); int oldKnotCount = bsp1->countKnots(); // Act // Try decreasing mult to zero. // NOTE: we still use OCCT notation of knot index starting with 1 (not 0). getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -1); // Assert // Knot should disappear. auto bsp2 = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp2->countKnots(), oldKnotCount - 1); } TEST_F(SketchObjectTest, testModifyKnotMultInPeriodicBSplineToDisallowed) { // Arrange auto PeriodicBSpline = createTypicalPeriodicBSpline(); assert(PeriodicBSpline); int geoId = getObject()->addGeometry(PeriodicBSpline.get()); // Act and Assert // TODO: Try modifying such that resultant multiplicity > degree // TODO: This should immediately throw exception EXPECT_THROW(getObject()->modifyBSplineKnotMultiplicity(geoId, 2, 3), Base::ValueError); // TODO: Try modifying such that resultant multiplicity < 0 // TODO: This should immediately throw exception EXPECT_THROW(getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -2), Base::ValueError); } TEST_F(SketchObjectTest, testModifyKnotMultInPeriodicBSpline) { // Arrange auto PeriodicBSpline = createTypicalPeriodicBSpline(); assert(PeriodicBSpline); int geoId = getObject()->addGeometry(PeriodicBSpline.get()); auto bsp = static_cast(getObject()->getGeometry(geoId)); int oldKnotsNum = bsp->countKnots(); int oldMultiplicityOfTargetKnot = bsp->getMultiplicities()[1]; // Act // TODO: Increase/decrease knot multiplicity normally getObject()->modifyBSplineKnotMultiplicity(geoId, 2, 1); // Assert // This should not alter the sizes of knot and multiplicity vectors. bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum); // This should increment the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[1], oldMultiplicityOfTargetKnot + 1); // This should still be a periodic spline EXPECT_TRUE(bsp->isPeriodic()); // TODO: Expect shape is preserved // Act // TODO: Increase/decrease knot multiplicity normally getObject()->modifyBSplineKnotMultiplicity(geoId, 2, -1); // Assert // This should not alter the sizes of knot and multiplicity vectors. bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum); // This should decrement the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[1], oldMultiplicityOfTargetKnot); // This should still be a non-periodic spline EXPECT_TRUE(bsp->isPeriodic()); } TEST_F(SketchObjectTest, testInsertKnotInNonPeriodicBSpline) { // Arrange auto nonPeriodicBSpline = createTypicalNonPeriodicBSpline(); assert(nonPeriodicBSpline); int geoId = getObject()->addGeometry(nonPeriodicBSpline.get()); // Act and Assert // Try inserting knot with zero multiplicity // zero multiplicity knot should immediately throw exception EXPECT_THROW(getObject()->insertBSplineKnot(geoId, 0.5, 0), Base::ValueError); // Act and Assert // Try inserting knot with multiplicity > degree // This should immediately throw exception EXPECT_THROW(getObject()->insertBSplineKnot(geoId, 0.5, 4), Base::ValueError); // Act and Assert // TODO: Try inserting at an existing knot with resultant multiplicity > degree // TODO: This should immediately throw exception // FIXME: Not happening. May be ignoring existing values. // EXPECT_THROW(getObject()->insertBSplineKnot(geoId, 1.0, 3), Base::ValueError); auto bsp = static_cast(getObject()->getGeometry(geoId)); int oldKnotsNum = bsp->countKnots(); int oldMultiplicityOfTargetKnot = bsp->getMultiplicities()[1]; // Act // Add at a general position (where no knot exists) getObject()->insertBSplineKnot(geoId, 0.5, 1); // Assert // This should add to both the knot and multiplicity "vectors" bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum + 1); // This should still be a non-periodic spline EXPECT_FALSE(bsp->isPeriodic()); // Act // Add a knot at an existing knot getObject()->insertBSplineKnot(geoId, 1.0, 1); // Assert // This should not alter the sizes of knot and multiplicity vectors. // (Since we previously added a knot, this means the total is still one more than original) bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum + 1); // This should increment the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[2], oldMultiplicityOfTargetKnot + 1); // This should still be a non-periodic spline EXPECT_FALSE(bsp->isPeriodic()); } TEST_F(SketchObjectTest, testInsertKnotInPeriodicBSpline) { // This should also cover as a representative of arc of conic // Arrange auto PeriodicBSpline = createTypicalPeriodicBSpline(); assert(PeriodicBSpline); int geoId = getObject()->addGeometry(PeriodicBSpline.get()); // Act and Assert // Try inserting knot with zero multiplicity // zero multiplicity knot should immediately throw exception EXPECT_THROW(getObject()->insertBSplineKnot(geoId, 0.5, 0), Base::ValueError); // Act and Assert // Try inserting knot with multiplicity > degree // This should immediately throw exception EXPECT_THROW(getObject()->insertBSplineKnot(geoId, 0.5, 4), Base::ValueError); // Act and Assert // TODO: Try inserting at an existing knot with resultant multiplicity > degree // TODO: This should immediately throw exception auto bsp = static_cast(getObject()->getGeometry(geoId)); int oldKnotsNum = bsp->countKnots(); int oldMultiplicityOfTargetKnot = bsp->getMultiplicities()[2]; // Act // Add at a general position (where no knot exists) getObject()->insertBSplineKnot(geoId, 0.5, 1); // Assert // This should add to both the knot and multiplicity "vectors" bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum + 1); // This should still be a periodic spline EXPECT_TRUE(bsp->isPeriodic()); // Act // Add a knot at an existing knot getObject()->insertBSplineKnot(geoId, 1.0, 1); // Assert // This should not alter the sizes of knot and multiplicity vectors. bsp = static_cast(getObject()->getGeometry(geoId)); EXPECT_EQ(bsp->countKnots(), oldKnotsNum + 1); // This should increment the multiplicity. EXPECT_EQ(bsp->getMultiplicities()[3], oldMultiplicityOfTargetKnot + 1); // This should still be a periodic spline EXPECT_TRUE(bsp->isPeriodic()); } TEST_F(SketchObjectTest, testJoinCurves) { // Arrange // Make two curves Base::Vector3d coordsCenter(0.0, 0.0, 0.0); double radius = 3.0, startParam = std::numbers::pi / 2, endParam = std::numbers::pi; Part::GeomArcOfCircle arcOfCircle; arcOfCircle.setCenter(coordsCenter); arcOfCircle.setRadius(radius); arcOfCircle.setRange(startParam, endParam, true); int geoId1 = getObject()->addGeometry(&arcOfCircle); Base::Vector3d coords1(0.1, 0.0, 0.0); Base::Vector3d coords2(3.0, 4.0, 0.0); Part::GeomLineSegment lineSeg; lineSeg.setPoints(coords1, coords2); int geoId2 = getObject()->addGeometry(&lineSeg); // Act // Join these curves getObject()->join(geoId1, Sketcher::PointPos::start, geoId2, Sketcher::PointPos::start); // Assert // Check they are replaced (here it means there is only one curve left after internal // geometries are removed) for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); } TEST_F(SketchObjectTest, testJoinCurvesWhenTangent) { // Arrange // Make two curves Base::Vector3d coordsCenter(0.0, 0.0, 0.0); double radius = 3.0, startParam = std::numbers::pi / 2, endParam = std::numbers::pi; Part::GeomArcOfCircle arcOfCircle; arcOfCircle.setCenter(coordsCenter); arcOfCircle.setRadius(radius); arcOfCircle.setRange(startParam, endParam, true); int geoId1 = getObject()->addGeometry(&arcOfCircle); Base::Vector3d coords1(0.0, 0.0, 0.0); Base::Vector3d coords2(3.0, 0.0, 0.0); Part::GeomLineSegment lineSeg; lineSeg.setPoints(coords1, coords2); int geoId2 = getObject()->addGeometry(&lineSeg); // Add end-to-end tangent between these auto constraint = new Sketcher::Constraint(); // Ownership will be transferred to the sketch constraint->Type = Sketcher::ConstraintType::Tangent; constraint->First = geoId1; constraint->FirstPos = Sketcher::PointPos::start; constraint->Second = geoId2; constraint->SecondPos = Sketcher::PointPos::start; getObject()->addConstraint(constraint); // Act // Join these curves getObject()->join(geoId1, Sketcher::PointPos::start, geoId2, Sketcher::PointPos::start, 1); // Assert // Check they are replaced (here it means there is only one curve left after internal // geometries are removed) for (int iterGeoId = 0; iterGeoId < getObject()->getHighestCurveIndex(); ++iterGeoId) { getObject()->deleteUnusedInternalGeometryAndUpdateGeoId(iterGeoId); } EXPECT_EQ(getObject()->getHighestCurveIndex(), 0); // TODO: Check the shape is conserved (how?) // Check there is no C-0 knot (should be possible for the chosen example) auto mults = static_cast(getObject()->getGeometry(0))->getMultiplicities(); EXPECT_TRUE(std::all_of(mults.begin(), mults.end(), [](auto mult) { return mult >= 1; })); }