977 lines
41 KiB
C++
977 lines
41 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* This file incorporates work covered by the following license notice:
|
|
*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed
|
|
* with this work for additional information regarding copyright
|
|
* ownership. The ASF licenses this file to you under the Apache
|
|
* License, Version 2.0 (the "License"); you may not use this file
|
|
* except in compliance with the License. You may obtain a copy of
|
|
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
|
|
*/
|
|
|
|
#include <osl/diagnose.h>
|
|
#include <sal/log.hxx>
|
|
#include <basegfx/polygon/b2dlinegeometry.hxx>
|
|
#include <basegfx/point/b2dpoint.hxx>
|
|
#include <basegfx/vector/b2dvector.hxx>
|
|
#include <basegfx/polygon/b2dpolygontools.hxx>
|
|
#include <basegfx/polygon/b2dpolypolygontools.hxx>
|
|
#include <basegfx/range/b2drange.hxx>
|
|
#include <basegfx/matrix/b2dhommatrix.hxx>
|
|
#include <basegfx/curve/b2dcubicbezier.hxx>
|
|
#include <basegfx/matrix/b2dhommatrixtools.hxx>
|
|
#include <com/sun/star/drawing/LineCap.hpp>
|
|
#include <basegfx/polygon/b2dpolypolygoncutter.hxx>
|
|
#include <basegfx/polygon/b2dpolygontriangulator.hxx>
|
|
|
|
namespace basegfx::utils
|
|
{
|
|
B2DPolyPolygon createAreaGeometryForLineStartEnd(
|
|
const B2DPolygon& rCandidate,
|
|
const B2DPolyPolygon& rArrow,
|
|
bool bStart,
|
|
double fWidth,
|
|
double fCandidateLength,
|
|
double fDockingPosition, // 0->top, 1->bottom
|
|
double* pConsumedLength,
|
|
double fShift)
|
|
{
|
|
B2DPolyPolygon aRetval;
|
|
assert((rCandidate.count() > 1) && "createAreaGeometryForLineStartEnd: Line polygon has too few points");
|
|
assert((rArrow.count() > 0) && "createAreaGeometryForLineStartEnd: Empty arrow utils::PolyPolygon");
|
|
assert((fWidth > 0.0) && "createAreaGeometryForLineStartEnd: Width too small");
|
|
assert((fDockingPosition >= 0.0 && fDockingPosition <= 1.0) &&
|
|
"createAreaGeometryForLineStartEnd: fDockingPosition out of range [0.0 .. 1.0]");
|
|
|
|
if(fWidth < 0.0)
|
|
{
|
|
fWidth = -fWidth;
|
|
}
|
|
|
|
if(rCandidate.count() > 1 && rArrow.count() && !fTools::equalZero(fWidth))
|
|
{
|
|
if(fDockingPosition < 0.0)
|
|
{
|
|
fDockingPosition = 0.0;
|
|
}
|
|
else if(fDockingPosition > 1.0)
|
|
{
|
|
fDockingPosition = 1.0;
|
|
}
|
|
|
|
// init return value from arrow
|
|
aRetval.append(rArrow);
|
|
|
|
// get size of the arrow
|
|
const B2DRange aArrowSize(getRange(rArrow));
|
|
|
|
// build ArrowTransform; center in X, align with axis in Y
|
|
B2DHomMatrix aArrowTransform(basegfx::utils::createTranslateB2DHomMatrix(
|
|
-aArrowSize.getCenter().getX(), -aArrowSize.getMinimum().getY()));
|
|
|
|
// scale to target size
|
|
const double fArrowScale(fWidth / (aArrowSize.getWidth()));
|
|
aArrowTransform.scale(fArrowScale, fArrowScale);
|
|
|
|
// get arrow size in Y
|
|
B2DPoint aUpperCenter(aArrowSize.getCenter().getX(), aArrowSize.getMaximum().getY());
|
|
aUpperCenter *= aArrowTransform;
|
|
const double fArrowYLength(B2DVector(aUpperCenter).getLength());
|
|
|
|
// move arrow to have docking position centered
|
|
aArrowTransform.translate(0.0, -fArrowYLength * fDockingPosition + fShift);
|
|
|
|
// prepare polygon length
|
|
if(fTools::equalZero(fCandidateLength))
|
|
{
|
|
fCandidateLength = getLength(rCandidate);
|
|
}
|
|
|
|
// get the polygon vector we want to plant this arrow on
|
|
const double fConsumedLength(fArrowYLength * (1.0 - fDockingPosition) - fShift);
|
|
const B2DVector aHead(rCandidate.getB2DPoint(bStart ? 0 : rCandidate.count() - 1));
|
|
const B2DVector aTail(getPositionAbsolute(rCandidate,
|
|
bStart ? fConsumedLength : fCandidateLength - fConsumedLength, fCandidateLength));
|
|
|
|
// from that vector, take the needed rotation and add rotate for arrow to transformation
|
|
const B2DVector aTargetDirection(aHead - aTail);
|
|
const double fRotation(atan2(aTargetDirection.getY(), aTargetDirection.getX()) + M_PI_2);
|
|
|
|
// rotate around docking position
|
|
aArrowTransform.rotate(fRotation);
|
|
|
|
// move arrow docking position to polygon head
|
|
aArrowTransform.translate(aHead.getX(), aHead.getY());
|
|
|
|
// transform retval and close
|
|
aRetval.transform(aArrowTransform);
|
|
aRetval.setClosed(true);
|
|
|
|
// if pConsumedLength is asked for, fill it
|
|
if(pConsumedLength)
|
|
{
|
|
*pConsumedLength = fConsumedLength;
|
|
}
|
|
}
|
|
|
|
return aRetval;
|
|
}
|
|
} // end of namespace
|
|
|
|
namespace basegfx
|
|
{
|
|
// anonymous namespace for local helpers
|
|
namespace
|
|
{
|
|
bool impIsSimpleEdge(const B2DCubicBezier& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad)
|
|
{
|
|
// isBezier() is true, already tested by caller
|
|
const B2DVector aEdge(rCandidate.getEndPoint() - rCandidate.getStartPoint());
|
|
|
|
if(aEdge.equalZero())
|
|
{
|
|
// start and end point the same, but control vectors used -> balloon curve loop
|
|
// is not a simple edge
|
|
return false;
|
|
}
|
|
|
|
// get tangentA and scalar with edge
|
|
const B2DVector aTangentA(rCandidate.getTangent(0.0));
|
|
const double fScalarAE(aEdge.scalar(aTangentA));
|
|
|
|
if(fScalarAE <= 0.0)
|
|
{
|
|
// angle between TangentA and Edge is bigger or equal 90 degrees
|
|
return false;
|
|
}
|
|
|
|
// get self-scalars for E and A
|
|
const double fScalarE(aEdge.scalar(aEdge));
|
|
const double fScalarA(aTangentA.scalar(aTangentA));
|
|
const double fLengthCompareE(fScalarE * fMaxPartOfEdgeQuad);
|
|
|
|
if(fTools::moreOrEqual(fScalarA, fLengthCompareE))
|
|
{
|
|
// length of TangentA is more than fMaxPartOfEdge of length of edge
|
|
return false;
|
|
}
|
|
|
|
if(fTools::lessOrEqual(fScalarAE * fScalarAE, fScalarA * fScalarE * fMaxCosQuad))
|
|
{
|
|
// angle between TangentA and Edge is bigger or equal angle defined by fMaxCos
|
|
return false;
|
|
}
|
|
|
|
// get tangentB and scalar with edge
|
|
const B2DVector aTangentB(rCandidate.getTangent(1.0));
|
|
const double fScalarBE(aEdge.scalar(aTangentB));
|
|
|
|
if(fScalarBE <= 0.0)
|
|
{
|
|
// angle between TangentB and Edge is bigger or equal 90 degrees
|
|
return false;
|
|
}
|
|
|
|
// get self-scalar for B
|
|
const double fScalarB(aTangentB.scalar(aTangentB));
|
|
|
|
if(fTools::moreOrEqual(fScalarB, fLengthCompareE))
|
|
{
|
|
// length of TangentB is more than fMaxPartOfEdge of length of edge
|
|
return false;
|
|
}
|
|
|
|
if(fTools::lessOrEqual(fScalarBE * fScalarBE, fScalarB * fScalarE * fMaxCosQuad))
|
|
{
|
|
// angle between TangentB and Edge is bigger or equal defined by fMaxCos
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void impSubdivideToSimple(const B2DCubicBezier& rCandidate, B2DPolygon& rTarget, double fMaxCosQuad, double fMaxPartOfEdgeQuad, sal_uInt32 nMaxRecursionDepth)
|
|
{
|
|
if(!nMaxRecursionDepth || impIsSimpleEdge(rCandidate, fMaxCosQuad, fMaxPartOfEdgeQuad))
|
|
{
|
|
rTarget.appendBezierSegment(rCandidate.getControlPointA(), rCandidate.getControlPointB(), rCandidate.getEndPoint());
|
|
}
|
|
else
|
|
{
|
|
B2DCubicBezier aLeft, aRight;
|
|
rCandidate.split(0.5, &aLeft, &aRight);
|
|
|
|
impSubdivideToSimple(aLeft, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1);
|
|
impSubdivideToSimple(aRight, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1);
|
|
}
|
|
}
|
|
|
|
B2DPolygon subdivideToSimple(const B2DPolygon& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad)
|
|
{
|
|
const sal_uInt32 nPointCount(rCandidate.count());
|
|
|
|
if(rCandidate.areControlPointsUsed() && nPointCount)
|
|
{
|
|
const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1);
|
|
B2DPolygon aRetval;
|
|
B2DCubicBezier aEdge;
|
|
|
|
// prepare edge for loop
|
|
aEdge.setStartPoint(rCandidate.getB2DPoint(0));
|
|
aRetval.append(aEdge.getStartPoint());
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount; a++)
|
|
{
|
|
// fill B2DCubicBezier
|
|
const sal_uInt32 nNextIndex((a + 1) % nPointCount);
|
|
aEdge.setControlPointA(rCandidate.getNextControlPoint(a));
|
|
aEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex));
|
|
aEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex));
|
|
|
|
// get rid of unnecessary bezier segments
|
|
aEdge.testAndSolveTrivialBezier();
|
|
|
|
if(aEdge.isBezier())
|
|
{
|
|
// before splitting recursively with internal simple criteria, use
|
|
// ExtremumPosFinder to remove those
|
|
std::vector< double > aExtremumPositions;
|
|
|
|
aExtremumPositions.reserve(4);
|
|
aEdge.getAllExtremumPositions(aExtremumPositions);
|
|
|
|
const sal_uInt32 nCount(aExtremumPositions.size());
|
|
|
|
if(nCount)
|
|
{
|
|
if(nCount > 1)
|
|
{
|
|
// create order from left to right
|
|
std::sort(aExtremumPositions.begin(), aExtremumPositions.end());
|
|
}
|
|
|
|
for(sal_uInt32 b(0); b < nCount;)
|
|
{
|
|
// split aEdge at next split pos
|
|
B2DCubicBezier aLeft;
|
|
const double fSplitPos(aExtremumPositions[b++]);
|
|
|
|
aEdge.split(fSplitPos, &aLeft, &aEdge);
|
|
aLeft.testAndSolveTrivialBezier();
|
|
|
|
// consume left part
|
|
if(aLeft.isBezier())
|
|
{
|
|
impSubdivideToSimple(aLeft, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6);
|
|
}
|
|
else
|
|
{
|
|
aRetval.append(aLeft.getEndPoint());
|
|
}
|
|
|
|
if(b < nCount)
|
|
{
|
|
// correct the remaining split positions to fit to shortened aEdge
|
|
const double fScaleFactor(1.0 / (1.0 - fSplitPos));
|
|
|
|
for(sal_uInt32 c(b); c < nCount; c++)
|
|
{
|
|
aExtremumPositions[c] = (aExtremumPositions[c] - fSplitPos) * fScaleFactor;
|
|
}
|
|
}
|
|
}
|
|
|
|
// test the shortened rest of aEdge
|
|
aEdge.testAndSolveTrivialBezier();
|
|
|
|
// consume right part
|
|
if(aEdge.isBezier())
|
|
{
|
|
impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6);
|
|
}
|
|
else
|
|
{
|
|
aRetval.append(aEdge.getEndPoint());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// straight edge, add point
|
|
aRetval.append(aEdge.getEndPoint());
|
|
}
|
|
|
|
// prepare edge for next step
|
|
aEdge.setStartPoint(aEdge.getEndPoint());
|
|
}
|
|
|
|
// copy closed flag and check for double points
|
|
aRetval.setClosed(rCandidate.isClosed());
|
|
aRetval.removeDoublePoints();
|
|
|
|
return aRetval;
|
|
}
|
|
else
|
|
{
|
|
return rCandidate;
|
|
}
|
|
}
|
|
|
|
B2DPolygon createAreaGeometryForEdge(
|
|
const B2DCubicBezier& rEdge,
|
|
double fHalfLineWidth,
|
|
bool bStartRound,
|
|
bool bEndRound,
|
|
bool bStartSquare,
|
|
bool bEndSquare)
|
|
{
|
|
// create polygon for edge
|
|
// Unfortunately, while it would be geometrically correct to not add
|
|
// the in-between points EdgeEnd and EdgeStart, it leads to rounding
|
|
// errors when converting to integer polygon coordinates for painting
|
|
if(rEdge.isBezier())
|
|
{
|
|
// prepare target and data common for upper and lower
|
|
B2DPolygon aBezierPolygon;
|
|
const B2DVector aPureEdgeVector(rEdge.getEndPoint() - rEdge.getStartPoint());
|
|
const double fEdgeLength(aPureEdgeVector.getLength());
|
|
const bool bIsEdgeLengthZero(fTools::equalZero(fEdgeLength));
|
|
B2DVector aTangentA(rEdge.getTangent(0.0)); aTangentA.normalize();
|
|
B2DVector aTangentB(rEdge.getTangent(1.0)); aTangentB.normalize();
|
|
const B2DVector aNormalizedPerpendicularA(getPerpendicular(aTangentA));
|
|
const B2DVector aNormalizedPerpendicularB(getPerpendicular(aTangentB));
|
|
|
|
// create upper displacement vectors and check if they cut
|
|
const B2DVector aPerpendStartA(aNormalizedPerpendicularA * -fHalfLineWidth);
|
|
const B2DVector aPerpendEndA(aNormalizedPerpendicularB * -fHalfLineWidth);
|
|
double fCutA(0.0);
|
|
const CutFlagValue aCutA(utils::findCut(
|
|
rEdge.getStartPoint(), aPerpendStartA,
|
|
rEdge.getEndPoint(), aPerpendEndA,
|
|
CutFlagValue::ALL, &fCutA));
|
|
const bool bCutA(aCutA != CutFlagValue::NONE);
|
|
|
|
// create lower displacement vectors and check if they cut
|
|
const B2DVector aPerpendStartB(aNormalizedPerpendicularA * fHalfLineWidth);
|
|
const B2DVector aPerpendEndB(aNormalizedPerpendicularB * fHalfLineWidth);
|
|
double fCutB(0.0);
|
|
const CutFlagValue aCutB(utils::findCut(
|
|
rEdge.getEndPoint(), aPerpendEndB,
|
|
rEdge.getStartPoint(), aPerpendStartB,
|
|
CutFlagValue::ALL, &fCutB));
|
|
const bool bCutB(aCutB != CutFlagValue::NONE);
|
|
|
|
// check if cut happens
|
|
const bool bCut(bCutA || bCutB);
|
|
B2DPoint aCutPoint;
|
|
|
|
// create left edge
|
|
if(bStartRound || bStartSquare)
|
|
{
|
|
if(bStartRound)
|
|
{
|
|
basegfx::B2DPolygon aStartPolygon(utils::createHalfUnitCircle());
|
|
|
|
aStartPolygon.transform(
|
|
utils::createScaleShearXRotateTranslateB2DHomMatrix(
|
|
fHalfLineWidth, fHalfLineWidth,
|
|
0.0,
|
|
atan2(aTangentA.getY(), aTangentA.getX()) + M_PI_2,
|
|
rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY()));
|
|
aBezierPolygon.append(aStartPolygon);
|
|
}
|
|
else // bStartSquare
|
|
{
|
|
const basegfx::B2DPoint aStart(rEdge.getStartPoint() - (aTangentA * fHalfLineWidth));
|
|
|
|
if(bCutB)
|
|
{
|
|
aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartB);
|
|
}
|
|
|
|
aBezierPolygon.append(aStart + aPerpendStartB);
|
|
aBezierPolygon.append(aStart + aPerpendStartA);
|
|
|
|
if(bCutA)
|
|
{
|
|
aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartA);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// append original in-between point
|
|
aBezierPolygon.append(rEdge.getStartPoint());
|
|
}
|
|
|
|
// create upper edge.
|
|
{
|
|
if(bCutA)
|
|
{
|
|
// calculate cut point and add
|
|
aCutPoint = rEdge.getStartPoint() + (aPerpendStartA * fCutA);
|
|
aBezierPolygon.append(aCutPoint);
|
|
}
|
|
else
|
|
{
|
|
// create scaled bezier segment
|
|
const B2DPoint aStart(rEdge.getStartPoint() + aPerpendStartA);
|
|
const B2DPoint aEnd(rEdge.getEndPoint() + aPerpendEndA);
|
|
const B2DVector aEdge(aEnd - aStart);
|
|
const double fLength(aEdge.getLength());
|
|
const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength);
|
|
const B2DVector fRelNext(rEdge.getControlPointA() - rEdge.getStartPoint());
|
|
const B2DVector fRelPrev(rEdge.getControlPointB() - rEdge.getEndPoint());
|
|
|
|
aBezierPolygon.append(aStart);
|
|
aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd);
|
|
}
|
|
}
|
|
|
|
// create right edge
|
|
if(bEndRound || bEndSquare)
|
|
{
|
|
if(bEndRound)
|
|
{
|
|
basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle());
|
|
|
|
aEndPolygon.transform(
|
|
utils::createScaleShearXRotateTranslateB2DHomMatrix(
|
|
fHalfLineWidth, fHalfLineWidth,
|
|
0.0,
|
|
atan2(aTangentB.getY(), aTangentB.getX()) - M_PI_2,
|
|
rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY()));
|
|
aBezierPolygon.append(aEndPolygon);
|
|
}
|
|
else // bEndSquare
|
|
{
|
|
const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + (aTangentB * fHalfLineWidth));
|
|
|
|
if(bCutA)
|
|
{
|
|
aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndA);
|
|
}
|
|
|
|
aBezierPolygon.append(aEnd + aPerpendEndA);
|
|
aBezierPolygon.append(aEnd + aPerpendEndB);
|
|
|
|
if(bCutB)
|
|
{
|
|
aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndB);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// append original in-between point
|
|
aBezierPolygon.append(rEdge.getEndPoint());
|
|
}
|
|
|
|
// create lower edge.
|
|
{
|
|
if(bCutB)
|
|
{
|
|
// calculate cut point and add
|
|
aCutPoint = rEdge.getEndPoint() + (aPerpendEndB * fCutB);
|
|
aBezierPolygon.append(aCutPoint);
|
|
}
|
|
else
|
|
{
|
|
// create scaled bezier segment
|
|
const B2DPoint aStart(rEdge.getEndPoint() + aPerpendEndB);
|
|
const B2DPoint aEnd(rEdge.getStartPoint() + aPerpendStartB);
|
|
const B2DVector aEdge(aEnd - aStart);
|
|
const double fLength(aEdge.getLength());
|
|
const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength);
|
|
const B2DVector fRelNext(rEdge.getControlPointB() - rEdge.getEndPoint());
|
|
const B2DVector fRelPrev(rEdge.getControlPointA() - rEdge.getStartPoint());
|
|
|
|
aBezierPolygon.append(aStart);
|
|
aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd);
|
|
}
|
|
}
|
|
|
|
// close
|
|
aBezierPolygon.setClosed(true);
|
|
|
|
if(bStartRound || bEndRound)
|
|
{
|
|
// double points possible when round caps are used at start or end
|
|
aBezierPolygon.removeDoublePoints();
|
|
}
|
|
|
|
if(bCut && ((bStartRound || bStartSquare) && (bEndRound || bEndSquare)))
|
|
{
|
|
// When cut exists and both ends are extended with caps, a self-intersecting polygon
|
|
// is created; one cut point is known, but there is a 2nd one in the caps geometry.
|
|
// Solve by using tooling.
|
|
// Remark: This nearly never happens due to curve preparations to extreme points
|
|
// and maximum angle turning, but I constructed a test case and checked that it is
|
|
// working properly.
|
|
const B2DPolyPolygon aTemp(utils::solveCrossovers(aBezierPolygon));
|
|
const sal_uInt32 nTempCount(aTemp.count());
|
|
|
|
if(nTempCount)
|
|
{
|
|
if(nTempCount > 1)
|
|
{
|
|
// as expected, multiple polygons (with same orientation). Remove
|
|
// the one which contains aCutPoint, or better take the one without
|
|
for (sal_uInt32 a(0); a < nTempCount; a++)
|
|
{
|
|
aBezierPolygon = aTemp.getB2DPolygon(a);
|
|
|
|
const sal_uInt32 nCandCount(aBezierPolygon.count());
|
|
|
|
for(sal_uInt32 b(0); b < nCandCount; b++)
|
|
{
|
|
if(aCutPoint.equal(aBezierPolygon.getB2DPoint(b)))
|
|
{
|
|
aBezierPolygon.clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(aBezierPolygon.count())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
OSL_ENSURE(aBezierPolygon.count(), "Error in line geometry creation, could not solve self-intersection (!)");
|
|
}
|
|
else
|
|
{
|
|
// none found, use result
|
|
aBezierPolygon = aTemp.getB2DPolygon(0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OSL_ENSURE(false, "Error in line geometry creation, could not solve self-intersection (!)");
|
|
}
|
|
}
|
|
|
|
// return
|
|
return aBezierPolygon;
|
|
}
|
|
else
|
|
{
|
|
// Get start and end point, create tangent and set to needed length
|
|
B2DVector aTangent(rEdge.getEndPoint() - rEdge.getStartPoint());
|
|
aTangent.setLength(fHalfLineWidth);
|
|
|
|
// prepare return value
|
|
B2DPolygon aEdgePolygon;
|
|
|
|
// buffered angle
|
|
double fAngle(0.0);
|
|
bool bAngle(false);
|
|
|
|
// buffered perpendicular
|
|
B2DVector aPerpend;
|
|
bool bPerpend(false);
|
|
|
|
// create left vertical
|
|
if(bStartRound)
|
|
{
|
|
aEdgePolygon = utils::createHalfUnitCircle();
|
|
fAngle = atan2(aTangent.getY(), aTangent.getX());
|
|
bAngle = true;
|
|
aEdgePolygon.transform(
|
|
utils::createScaleShearXRotateTranslateB2DHomMatrix(
|
|
fHalfLineWidth, fHalfLineWidth,
|
|
0.0,
|
|
fAngle + M_PI_2,
|
|
rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY()));
|
|
}
|
|
else
|
|
{
|
|
aPerpend.setX(-aTangent.getY());
|
|
aPerpend.setY(aTangent.getX());
|
|
bPerpend = true;
|
|
|
|
if(bStartSquare)
|
|
{
|
|
const basegfx::B2DPoint aStart(rEdge.getStartPoint() - aTangent);
|
|
|
|
aEdgePolygon.append(aStart + aPerpend);
|
|
aEdgePolygon.append(aStart - aPerpend);
|
|
}
|
|
else
|
|
{
|
|
aEdgePolygon.append(rEdge.getStartPoint() + aPerpend);
|
|
aEdgePolygon.append(rEdge.getStartPoint()); // keep the in-between point for numerical reasons
|
|
aEdgePolygon.append(rEdge.getStartPoint() - aPerpend);
|
|
}
|
|
}
|
|
|
|
// create right vertical
|
|
if(bEndRound)
|
|
{
|
|
basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle());
|
|
|
|
if(!bAngle)
|
|
{
|
|
fAngle = atan2(aTangent.getY(), aTangent.getX());
|
|
}
|
|
|
|
aEndPolygon.transform(
|
|
utils::createScaleShearXRotateTranslateB2DHomMatrix(
|
|
fHalfLineWidth, fHalfLineWidth,
|
|
0.0,
|
|
fAngle - M_PI_2,
|
|
rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY()));
|
|
aEdgePolygon.append(aEndPolygon);
|
|
}
|
|
else
|
|
{
|
|
if(!bPerpend)
|
|
{
|
|
aPerpend.setX(-aTangent.getY());
|
|
aPerpend.setY(aTangent.getX());
|
|
}
|
|
|
|
if(bEndSquare)
|
|
{
|
|
const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + aTangent);
|
|
|
|
aEdgePolygon.append(aEnd - aPerpend);
|
|
aEdgePolygon.append(aEnd + aPerpend);
|
|
}
|
|
else
|
|
{
|
|
aEdgePolygon.append(rEdge.getEndPoint() - aPerpend);
|
|
aEdgePolygon.append(rEdge.getEndPoint()); // keep the in-between point for numerical reasons
|
|
aEdgePolygon.append(rEdge.getEndPoint() + aPerpend);
|
|
}
|
|
}
|
|
|
|
// close and return
|
|
aEdgePolygon.setClosed(true);
|
|
|
|
return aEdgePolygon;
|
|
}
|
|
}
|
|
|
|
B2DPolygon createAreaGeometryForJoin(
|
|
const B2DVector& rTangentPrev,
|
|
const B2DVector& rTangentEdge,
|
|
const B2DVector& rPerpendPrev,
|
|
const B2DVector& rPerpendEdge,
|
|
const B2DPoint& rPoint,
|
|
double fHalfLineWidth,
|
|
B2DLineJoin eJoin,
|
|
double fMiterMinimumAngle)
|
|
{
|
|
SAL_WARN_IF(fHalfLineWidth <= 0.0,"basegfx","createAreaGeometryForJoin: LineWidth too small (!)");
|
|
assert((eJoin != B2DLineJoin::NONE) && "createAreaGeometryForJoin: B2DLineJoin::NONE not allowed (!)");
|
|
|
|
// LineJoin from tangent rPerpendPrev to tangent rPerpendEdge in rPoint
|
|
B2DPolygon aEdgePolygon;
|
|
const B2DPoint aStartPoint(rPoint + rPerpendPrev);
|
|
const B2DPoint aEndPoint(rPoint + rPerpendEdge);
|
|
|
|
// test if for Miter, the angle is too small and the fallback
|
|
// to bevel needs to be used
|
|
if(eJoin == B2DLineJoin::Miter)
|
|
{
|
|
const double fAngle(fabs(rPerpendPrev.angle(rPerpendEdge)));
|
|
|
|
if((M_PI - fAngle) < fMiterMinimumAngle)
|
|
{
|
|
// fallback to bevel
|
|
eJoin = B2DLineJoin::Bevel;
|
|
}
|
|
}
|
|
|
|
switch(eJoin)
|
|
{
|
|
case B2DLineJoin::Miter :
|
|
{
|
|
aEdgePolygon.append(aEndPoint);
|
|
aEdgePolygon.append(rPoint);
|
|
aEdgePolygon.append(aStartPoint);
|
|
|
|
// Look for the cut point between start point along rTangentPrev and
|
|
// end point along rTangentEdge. -rTangentEdge should be used, but since
|
|
// the cut value is used for interpolating along the first edge, the negation
|
|
// is not needed since the same fCut will be found on the first edge.
|
|
// If it exists, insert it to complete the mitered fill polygon.
|
|
double fCutPos(0.0);
|
|
utils::findCut(aStartPoint, rTangentPrev, aEndPoint, rTangentEdge, CutFlagValue::ALL, &fCutPos);
|
|
|
|
if(fCutPos != 0.0)
|
|
{
|
|
const B2DPoint aCutPoint(aStartPoint + (rTangentPrev * fCutPos));
|
|
aEdgePolygon.append(aCutPoint);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case B2DLineJoin::Round :
|
|
{
|
|
// use tooling to add needed EllipseSegment
|
|
double fAngleStart(atan2(rPerpendPrev.getY(), rPerpendPrev.getX()));
|
|
double fAngleEnd(atan2(rPerpendEdge.getY(), rPerpendEdge.getX()));
|
|
|
|
// atan2 results are [-PI .. PI], consolidate to [0.0 .. 2PI]
|
|
if(fAngleStart < 0.0)
|
|
{
|
|
fAngleStart += 2 * M_PI;
|
|
}
|
|
|
|
if(fAngleEnd < 0.0)
|
|
{
|
|
fAngleEnd += 2 * M_PI;
|
|
}
|
|
|
|
const B2DPolygon aBow(utils::createPolygonFromEllipseSegment(rPoint, fHalfLineWidth, fHalfLineWidth, fAngleStart, fAngleEnd));
|
|
|
|
if(aBow.count() > 1)
|
|
{
|
|
// #i101491#
|
|
// use the original start/end positions; the ones from bow creation may be numerically
|
|
// different due to their different creation. To guarantee good merging quality with edges
|
|
// and edge roundings (and to reduce point count)
|
|
aEdgePolygon = aBow;
|
|
aEdgePolygon.setB2DPoint(0, aStartPoint);
|
|
aEdgePolygon.setB2DPoint(aEdgePolygon.count() - 1, aEndPoint);
|
|
aEdgePolygon.append(rPoint);
|
|
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
[[fallthrough]]; // wanted fall-through to default
|
|
}
|
|
}
|
|
default: // B2DLineJoin::Bevel
|
|
{
|
|
aEdgePolygon.append(aEndPoint);
|
|
aEdgePolygon.append(rPoint);
|
|
aEdgePolygon.append(aStartPoint);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// create last polygon part for edge
|
|
aEdgePolygon.setClosed(true);
|
|
|
|
return aEdgePolygon;
|
|
}
|
|
} // end of anonymous namespace
|
|
|
|
namespace utils
|
|
{
|
|
B2DPolyPolygon createAreaGeometry(
|
|
const B2DPolygon& rCandidate,
|
|
double fHalfLineWidth,
|
|
B2DLineJoin eJoin,
|
|
css::drawing::LineCap eCap,
|
|
double fMaxAllowedAngle,
|
|
double fMaxPartOfEdge,
|
|
double fMiterMinimumAngle)
|
|
{
|
|
if(fMaxAllowedAngle > M_PI_2)
|
|
{
|
|
fMaxAllowedAngle = M_PI_2;
|
|
}
|
|
else if(fMaxAllowedAngle < 0.01 * M_PI_2)
|
|
{
|
|
fMaxAllowedAngle = 0.01 * M_PI_2;
|
|
}
|
|
|
|
if(fMaxPartOfEdge > 1.0)
|
|
{
|
|
fMaxPartOfEdge = 1.0;
|
|
}
|
|
else if(fMaxPartOfEdge < 0.01)
|
|
{
|
|
fMaxPartOfEdge = 0.01;
|
|
}
|
|
|
|
if(fMiterMinimumAngle > M_PI)
|
|
{
|
|
fMiterMinimumAngle = M_PI;
|
|
}
|
|
else if(fMiterMinimumAngle < 0.01 * M_PI)
|
|
{
|
|
fMiterMinimumAngle = 0.01 * M_PI;
|
|
}
|
|
|
|
B2DPolygon aCandidate(rCandidate);
|
|
const double fMaxCos(cos(fMaxAllowedAngle));
|
|
|
|
aCandidate.removeDoublePoints();
|
|
aCandidate = subdivideToSimple(aCandidate, fMaxCos * fMaxCos, fMaxPartOfEdge * fMaxPartOfEdge);
|
|
|
|
const sal_uInt32 nPointCount(aCandidate.count());
|
|
|
|
if(nPointCount)
|
|
{
|
|
B2DPolyPolygon aRetval;
|
|
const bool bIsClosed(aCandidate.isClosed());
|
|
const sal_uInt32 nEdgeCount(bIsClosed ? nPointCount : nPointCount - 1);
|
|
const bool bLineCap(!bIsClosed && eCap != css::drawing::LineCap_BUTT);
|
|
|
|
if(nEdgeCount)
|
|
{
|
|
B2DCubicBezier aEdge;
|
|
B2DCubicBezier aPrev;
|
|
|
|
const bool bEventuallyCreateLineJoin(eJoin != B2DLineJoin::NONE);
|
|
// prepare edge
|
|
aEdge.setStartPoint(aCandidate.getB2DPoint(0));
|
|
|
|
if(bIsClosed && bEventuallyCreateLineJoin)
|
|
{
|
|
// prepare previous edge
|
|
const sal_uInt32 nPrevIndex(nPointCount - 1);
|
|
aPrev.setStartPoint(aCandidate.getB2DPoint(nPrevIndex));
|
|
aPrev.setControlPointA(aCandidate.getNextControlPoint(nPrevIndex));
|
|
aPrev.setControlPointB(aCandidate.getPrevControlPoint(0));
|
|
aPrev.setEndPoint(aEdge.getStartPoint());
|
|
}
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount; a++)
|
|
{
|
|
// fill current Edge
|
|
const sal_uInt32 nNextIndex((a + 1) % nPointCount);
|
|
aEdge.setControlPointA(aCandidate.getNextControlPoint(a));
|
|
aEdge.setControlPointB(aCandidate.getPrevControlPoint(nNextIndex));
|
|
aEdge.setEndPoint(aCandidate.getB2DPoint(nNextIndex));
|
|
|
|
// check and create linejoin
|
|
if(bEventuallyCreateLineJoin && (bIsClosed || a != 0))
|
|
{
|
|
B2DVector aTangentPrev(aPrev.getTangent(1.0)); aTangentPrev.normalize();
|
|
B2DVector aTangentEdge(aEdge.getTangent(0.0)); aTangentEdge.normalize();
|
|
B2VectorOrientation aOrientation(getOrientation(aTangentPrev, aTangentEdge));
|
|
|
|
if(aOrientation == B2VectorOrientation::Neutral)
|
|
{
|
|
// they are parallel or empty; if they are both not zero and point
|
|
// in opposite direction, a half-circle is needed
|
|
if(!aTangentPrev.equalZero() && !aTangentEdge.equalZero())
|
|
{
|
|
const double fAngle(fabs(aTangentPrev.angle(aTangentEdge)));
|
|
|
|
if(fTools::equal(fAngle, M_PI))
|
|
{
|
|
// for half-circle production, fallback to positive
|
|
// orientation
|
|
aOrientation = B2VectorOrientation::Positive;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(aOrientation == B2VectorOrientation::Positive)
|
|
{
|
|
const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * -fHalfLineWidth);
|
|
const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * -fHalfLineWidth);
|
|
|
|
aRetval.append(
|
|
createAreaGeometryForJoin(
|
|
aTangentPrev,
|
|
aTangentEdge,
|
|
aPerpendPrev,
|
|
aPerpendEdge,
|
|
aEdge.getStartPoint(),
|
|
fHalfLineWidth,
|
|
eJoin,
|
|
fMiterMinimumAngle));
|
|
}
|
|
else if(aOrientation == B2VectorOrientation::Negative)
|
|
{
|
|
const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * fHalfLineWidth);
|
|
const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * fHalfLineWidth);
|
|
|
|
aRetval.append(
|
|
createAreaGeometryForJoin(
|
|
aTangentEdge,
|
|
aTangentPrev,
|
|
aPerpendEdge,
|
|
aPerpendPrev,
|
|
aEdge.getStartPoint(),
|
|
fHalfLineWidth,
|
|
eJoin,
|
|
fMiterMinimumAngle));
|
|
}
|
|
}
|
|
|
|
// create geometry for edge
|
|
const bool bLast(a + 1 == nEdgeCount);
|
|
|
|
if(bLineCap)
|
|
{
|
|
const bool bFirst(!a);
|
|
|
|
aRetval.append(
|
|
createAreaGeometryForEdge(
|
|
aEdge,
|
|
fHalfLineWidth,
|
|
bFirst && eCap == css::drawing::LineCap_ROUND,
|
|
bLast && eCap == css::drawing::LineCap_ROUND,
|
|
bFirst && eCap == css::drawing::LineCap_SQUARE,
|
|
bLast && eCap == css::drawing::LineCap_SQUARE));
|
|
}
|
|
else
|
|
{
|
|
aRetval.append(
|
|
createAreaGeometryForEdge(
|
|
aEdge,
|
|
fHalfLineWidth,
|
|
false,
|
|
false,
|
|
false,
|
|
false));
|
|
}
|
|
|
|
// prepare next step
|
|
if(!bLast)
|
|
{
|
|
if(bEventuallyCreateLineJoin)
|
|
{
|
|
aPrev = aEdge;
|
|
}
|
|
|
|
aEdge.setStartPoint(aEdge.getEndPoint());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// point count, but no edge count -> single point
|
|
const basegfx::B2DPolygon aCircle(
|
|
createPolygonFromCircle(
|
|
aCandidate.getB2DPoint(0),
|
|
fHalfLineWidth));
|
|
|
|
aRetval.append(aCircle);
|
|
}
|
|
|
|
return aRetval;
|
|
}
|
|
else
|
|
{
|
|
return B2DPolyPolygon(rCandidate);
|
|
}
|
|
}
|
|
} // end of namespace utils
|
|
} // end of namespace basegfx
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|