tdf#103474 handle edge cases in ARCANGLETO

The arc-polygon generation in tools does not consider edge cases with
zero height or width. That leads to wrong rendering in some ooxml
shapes, when the handle is dragged to its extrem position, e.g. in
left/right braces/bracket and can.

I have switched from tools to basegfx in case ARCANGLETO and added
handling for edge cases. Switching to basegfx has the additional
advantage, that Bezier curves are used and not polylines. You see
the difference, if you convert the shape to curve.
ARCANGLETO is not used from our shapes or from import from binary
MS Office, but only from OOXML or user-defined custom shapes.

tdf#122323 MS Office restricts the swing angle to [-360°,360] in
rendering. Such restriction is not in OOXML and not in ODF.
Nevertheless, I have added a clamp for ooxml-foo shapes for better
interoperability.

Change-Id: Ib3233ce14dab950cc521cb8cbac6809a1d3e34a7
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/96068
Tested-by: Jenkins
Reviewed-by: Regina Henschel <rb.henschel@t-online.de>
diff --git a/svx/qa/unit/customshapes.cxx b/svx/qa/unit/customshapes.cxx
index 5382636..79cd6de 100644
--- a/svx/qa/unit/customshapes.cxx
+++ b/svx/qa/unit/customshapes.cxx
@@ -696,6 +696,59 @@
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aStart y-coordinate", 9999.0, aStart.getY(), 1.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aEnd y-coordinate", 1999.0, aEnd.getY(), 1.0);
}

CPPUNIT_TEST_FIXTURE(CustomshapesTest, testTdf103474_commandG_CaseZeroHeight)
{
    // Some as above, but with shape with command G.
    OUString sURL
        = m_directories.getURLFromSrc(sDataDirectory) + "tdf103474_commandG_CaseZeroHeight.odp";
    mxComponent = loadFromDesktop(sURL, "com.sun.star.comp.presentation.PresentationDocument");
    CPPUNIT_ASSERT_MESSAGE("Could not load document", mxComponent.is());
    uno::Reference<drawing::XShape> xShape(getShape(0));
    // The end points of the straight line segment should have the same x-coordinate of left
    // of shape, and different y-coordinates, one top and the other bottom of the shape.
    SdrObjCustomShape& rSdrObjCustomShape(
        static_cast<SdrObjCustomShape&>(*GetSdrObjectFromXShape(xShape)));
    EnhancedCustomShape2d aCustomShape2d(rSdrObjCustomShape);
    SdrPathObj* pPathObj = static_cast<SdrPathObj*>(aCustomShape2d.CreateLineGeometry());
    CPPUNIT_ASSERT_MESSAGE("Could not convert to SdrPathObj", pPathObj);
    const basegfx::B2DPolyPolygon aPolyPolygon(pPathObj->GetPathPoly());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("count polygons", static_cast<sal_uInt32>(1),
                                 aPolyPolygon.count());
    const basegfx::B2DPolygon aPolygon(aPolyPolygon.getB2DPolygon(0));
    // Get the middle points of the polygon. They are the endpoints of the
    // straight line segment regardless of the quarter ellipse parts, because
    // the shape is symmetric.
    const basegfx::B2DPoint aStart(aPolygon.getB2DPoint(aPolygon.count() / 2 - 1));
    const basegfx::B2DPoint aEnd(aPolygon.getB2DPoint(aPolygon.count() / 2));
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aStart x-coordinate", 1999.0, aStart.getX(), 1.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aEnd x-coordinate", 1999.0, aEnd.getX(), 1.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aStart y-coordinate", 9999.0, aStart.getY(), 1.0);
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("aEnd y-coordinate", 1999.0, aEnd.getY(), 1.0);
}

CPPUNIT_TEST_FIXTURE(CustomshapesTest, testTdf122323_largeSwingAngle)
{
    // SwingAngles are clamped to [-360;360] in MS Office. Error was, that LO calculated
    // the end angle and used it modulo 360, no full ellipse was drawn.
    OUString sURL
        = m_directories.getURLFromSrc(sDataDirectory) + "tdf122323_swingAngle_larger360deg.pptx";
    mxComponent = loadFromDesktop(sURL, "com.sun.star.comp.presentation.PresentationDocument");
    CPPUNIT_ASSERT_MESSAGE("Could not load document", mxComponent.is());
    uno::Reference<drawing::XShape> xShape(getShape(0));
    uno::Reference<beans::XPropertySet> xShapeProps(xShape, uno::UNO_QUERY);
    SdrObjCustomShape& rSdrObjCustomShape(
        static_cast<SdrObjCustomShape&>(*GetSdrObjectFromXShape(xShape)));
    EnhancedCustomShape2d aCustomShape2d(rSdrObjCustomShape);
    SdrPathObj* pPathObj = static_cast<SdrPathObj*>(aCustomShape2d.CreateLineGeometry());
    CPPUNIT_ASSERT_MESSAGE("Could not convert to SdrPathObj", pPathObj);
    const basegfx::B2DPolyPolygon aPolyPolygon(pPathObj->GetPathPoly());
    const basegfx::B2DPolygon aPolygon(aPolyPolygon.getB2DPolygon(0));
    const basegfx::B2DPoint aStart(aPolygon.getB2DPoint(0));
    // last point comes from line to center, therefore -2 instead of -1
    const basegfx::B2DPoint aEnd(aPolygon.getB2DPoint(aPolygon.count() - 2));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Start <> End", aStart, aEnd);
}
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/svx/qa/unit/data/tdf103474_commandG_CaseZeroHeight.odp b/svx/qa/unit/data/tdf103474_commandG_CaseZeroHeight.odp
new file mode 100644
index 0000000..9b36d45
--- /dev/null
+++ b/svx/qa/unit/data/tdf103474_commandG_CaseZeroHeight.odp
Binary files differ
diff --git a/svx/qa/unit/data/tdf122323_swingAngle_larger360deg.pptx b/svx/qa/unit/data/tdf122323_swingAngle_larger360deg.pptx
new file mode 100644
index 0000000..919675e
--- /dev/null
+++ b/svx/qa/unit/data/tdf122323_swingAngle_larger360deg.pptx
Binary files differ
diff --git a/svx/source/customshapes/EnhancedCustomShape2d.cxx b/svx/source/customshapes/EnhancedCustomShape2d.cxx
index 8b570ed..89360b6 100644
--- a/svx/source/customshapes/EnhancedCustomShape2d.cxx
+++ b/svx/source/customshapes/EnhancedCustomShape2d.cxx
@@ -2356,58 +2356,80 @@

                case ARCANGLETO :
                {
                    double fWR, fHR, fStartAngle, fSwingAngle;
                    double fWR, fHR; // in Shape coordinate system
                    double fStartAngle, fSwingAngle; // in deg

                    for ( sal_uInt16 i = 0; ( i < nPntCount ) && ( rSrcPt + 1 < nCoordSize ); i++ )
                    {
                        GetParameter ( fWR, seqCoordinates[ static_cast<sal_uInt16>(rSrcPt) ].First, true, false );
                        GetParameter ( fHR, seqCoordinates[ static_cast<sal_uInt16>(rSrcPt) ].Second, false, true );
                        basegfx::B2DPoint aTempPair;
                        aTempPair = GetPointAsB2DPoint(seqCoordinates[static_cast<sal_uInt16>(rSrcPt)], false /*bScale*/, false /*bReplaceGeoSize*/);
                        fWR = aTempPair.getX();
                        fHR = aTempPair.getY();
                        aTempPair = GetPointAsB2DPoint(seqCoordinates[static_cast<sal_uInt16>(rSrcPt + 1)], false /*bScale*/, false /*bReplaceGeoSize*/);
                        fStartAngle = aTempPair.getX();
                        fSwingAngle = aTempPair.getY();

                        GetParameter ( fStartAngle, seqCoordinates[ static_cast<sal_uInt16>( rSrcPt + 1) ].First, false, false );
                        GetParameter ( fSwingAngle, seqCoordinates[ static_cast<sal_uInt16>( rSrcPt + 1 ) ].Second, false, false );

                        // Convert angles to radians, but don't do any scaling / translation yet.

                        fStartAngle = basegfx::deg2rad(fStartAngle);
                        fSwingAngle = basegfx::deg2rad(fSwingAngle);
                        // tdf#122323 MS Office clamps the swing angle to [-360,360]. Such restriction
                        // is neither in OOXML nor in ODF. Nevertheless, to be compatible we do it for
                        // "ooxml-foo" shapes. Those shapes have their origin in MS Office.
                        if (bOOXMLShape)
                        {
                            fSwingAngle = std::clamp(fSwingAngle, -360.0, 360.0);
                        }

                        SAL_INFO("svx", "ARCANGLETO scale: " << fWR << "x" << fHR << " angles: " << fStartAngle << "," << fSwingAngle);

                        bool bClockwise = fSwingAngle >= 0.0;

                        if (aNewB2DPolygon.count() > 0)
                        if (aNewB2DPolygon.count() > 0) // otherwise no "current point"
                        {
                            basegfx::B2DPoint aStartPointB2D( aNewB2DPolygon.getB2DPoint(aNewB2DPolygon.count() - 1 ) );
                            Point aStartPoint( 0, 0 );
                            // use similar methods as in command U
                            basegfx::B2DPolygon aTempB2DPolygon;

                            double fT = atan2((fWR*sin(fStartAngle)), (fHR*cos(fStartAngle)));
                            double fTE = atan2((fWR*sin(fStartAngle + fSwingAngle)), fHR*cos(fStartAngle + fSwingAngle));
                            if (fWR == 0.0 && fHR == 0.0)
                            {
                                // degenerated ellipse, add this one point
                                aTempB2DPolygon.append(basegfx::B2DPoint(0.0, 0.0));
                            }
                            else
                            {
                                double fEndAngle = fStartAngle + fSwingAngle;
                                // Generate arc with ellipse left|top = 0|0.
                                basegfx::B2DPoint aCenter(fWR, fHR);
                                if (fSwingAngle < 0.0)
                                    std::swap(fStartAngle, fEndAngle);
                                double fS; // fFrom in radians in [0..2Pi[
                                double fE; // fTo or fEndAngle in radians in [0..2PI[
                                double fFrom(fStartAngle);
                                // createPolygonFromEllipseSegment expects angles in [0..2PI[.
                                if (fSwingAngle >= 360.0 || fSwingAngle <= -360.0)
                                {
                                    double fTo(fFrom + 180.0);
                                    while (fTo < fEndAngle)
                                    {
                                        fS = lcl_getNormalizedCircleAngleRad(fWR, fHR, fFrom);
                                        fE = lcl_getNormalizedCircleAngleRad(fWR, fHR, fTo);
                                        aTempB2DPolygon.append(basegfx::utils::createPolygonFromEllipseSegment(aCenter, fWR, fHR, fS,fE));
                                        fFrom = fTo;
                                        fTo += 180.0;
                                    }
                                }
                                fS = lcl_getNormalizedCircleAngleRad(fWR, fHR, fFrom);
                                fE = lcl_getNormalizedCircleAngleRad(fWR, fHR, fEndAngle);
                                aTempB2DPolygon.append(basegfx::utils::createPolygonFromEllipseSegment(aCenter, fWR, fHR,fS, fE));
                                if (fSwingAngle < 0)
                                    aTempB2DPolygon.flip();
                                aTempB2DPolygon.removeDoublePoints();
                            }
                            // Scale arc to 1/100mm
                            basegfx::B2DHomMatrix aMatrix = basegfx::utils::createScaleB2DHomMatrix(fXScale, fYScale);
                            aTempB2DPolygon.transform(aMatrix);

                            SAL_INFO("svx", "ARCANGLETO angles: " << fStartAngle << ", " << fSwingAngle
                                             << " --> parameters: " << fT <<", " << fTE );

                            fWR *= fXScale;
                            fHR *= fYScale;

                            tools::Rectangle aRect ( Point ( aStartPoint.getX() - fWR*cos(fT) - fWR, aStartPoint.getY() - fHR*sin(fT) - fHR ),
                                              Point ( aStartPoint.getX() - fWR*cos(fT) + fWR, aStartPoint.getY() - fHR*sin(fT) + fHR) );

                            Point aEndPoint ( aStartPoint.getX() - fWR*(cos(fT) - cos(fTE)), aStartPoint.getY() - fHR*(sin(fT) - sin(fTE)) );

                            SAL_INFO(
                                "svx",
                                "ARCANGLETO rect: " << aRect.Left() << ", "
                                    << aRect.Top() << "   x   " << aRect.Right()
                                    << ", " << aRect.Bottom() << "   start: "
                                    << aStartPoint.X() << ", "
                                    << aStartPoint.Y() << " end: "
                                    << aEndPoint.X() << ", " << aEndPoint.Y()
                                    << " clockwise: " << int(bClockwise));
                            basegfx::B2DPolygon aArc = CreateArc( aRect, bClockwise ? aEndPoint : aStartPoint, bClockwise ? aStartPoint : aEndPoint, bClockwise, aStartPoint == aEndPoint && ((bClockwise && fSwingAngle > F_PI) || (!bClockwise && fSwingAngle < -F_PI)));
                            // Now that we have the arc, move it to aStartPointB2D.
                            basegfx::B2DHomMatrix aMatrix = basegfx::utils::createTranslateB2DHomMatrix(aStartPointB2D.getX(), aStartPointB2D.getY());
                            aArc.transform(aMatrix);
                            aNewB2DPolygon.append(aArc);
                            // Now that we have the arc, move it to the "current point".
                            basegfx::B2DPoint aCurrentPointB2D( aNewB2DPolygon.getB2DPoint(aNewB2DPolygon.count() - 1 ) );
                            const double fDx(aCurrentPointB2D.getX() - aTempB2DPolygon.getB2DPoint(0).getX());
                            const double fDy(aCurrentPointB2D.getY() - aTempB2DPolygon.getB2DPoint(0).getY());
                            aMatrix = basegfx::utils::createTranslateB2DHomMatrix(fDx, fDy);
                            aTempB2DPolygon.transform(aMatrix);
                            aNewB2DPolygon.append(aTempB2DPolygon);
                        }

                        rSrcPt += 2;