27ad68a95b
Change-Id: Id8811e65227142881fc86f6d2db231fa053fc2d3 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/154768 Tested-by: Jenkins Reviewed-by: Caolán McNamara <caolan.mcnamara@collabora.com>
1077 lines
48 KiB
C++
1077 lines
48 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 <basegfx/polygon/b2dpolygoncutandtouch.hxx>
|
|
#include <osl/diagnose.h>
|
|
#include <sal/log.hxx>
|
|
#include <basegfx/numeric/ftools.hxx>
|
|
#include <basegfx/point/b2dpoint.hxx>
|
|
#include <basegfx/vector/b2dvector.hxx>
|
|
#include <basegfx/range/b2drange.hxx>
|
|
#include <basegfx/polygon/b2dpolygontools.hxx>
|
|
#include <basegfx/curve/b2dcubicbezier.hxx>
|
|
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
|
|
#define SUBDIVIDE_FOR_CUT_TEST_COUNT (50)
|
|
|
|
namespace basegfx
|
|
{
|
|
namespace
|
|
{
|
|
|
|
class temporaryPoint
|
|
{
|
|
B2DPoint maPoint; // the new point
|
|
sal_uInt32 mnIndex; // index after which to insert
|
|
double mfCut; // parametric cut description [0.0 .. 1.0]
|
|
|
|
public:
|
|
temporaryPoint(const B2DPoint& rNewPoint, sal_uInt32 nIndex, double fCut)
|
|
: maPoint(rNewPoint),
|
|
mnIndex(nIndex),
|
|
mfCut(fCut)
|
|
{
|
|
}
|
|
|
|
bool operator<(const temporaryPoint& rComp) const
|
|
{
|
|
if(mnIndex == rComp.mnIndex)
|
|
{
|
|
return (mfCut < rComp.mfCut);
|
|
}
|
|
|
|
return (mnIndex < rComp.mnIndex);
|
|
}
|
|
|
|
const B2DPoint& getPoint() const { return maPoint; }
|
|
sal_uInt32 getIndex() const { return mnIndex; }
|
|
double getCut() const { return mfCut; }
|
|
};
|
|
|
|
typedef std::vector< temporaryPoint > temporaryPointVector;
|
|
|
|
class temporaryPolygonData
|
|
{
|
|
B2DPolygon maPolygon;
|
|
B2DRange maRange;
|
|
temporaryPointVector maPoints;
|
|
|
|
public:
|
|
const B2DPolygon& getPolygon() const { return maPolygon; }
|
|
void setPolygon(const B2DPolygon& rNew) { maPolygon = rNew; maRange = utils::getRange(maPolygon); }
|
|
const B2DRange& getRange() const { return maRange; }
|
|
temporaryPointVector& getTemporaryPointVector() { return maPoints; }
|
|
};
|
|
|
|
B2DPolygon mergeTemporaryPointsAndPolygon(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints)
|
|
{
|
|
// #i76891# mergeTemporaryPointsAndPolygon redesigned to be able to correctly handle
|
|
// single edges with/without control points
|
|
// #i101491# added counter for non-changing element count
|
|
const sal_uInt32 nTempPointCount(rTempPoints.size());
|
|
|
|
if(nTempPointCount)
|
|
{
|
|
B2DPolygon aRetval;
|
|
const sal_uInt32 nCount(rCandidate.count());
|
|
|
|
if(nCount)
|
|
{
|
|
// sort temp points to assure increasing fCut values and increasing indices
|
|
std::sort(rTempPoints.begin(), rTempPoints.end());
|
|
|
|
// prepare loop
|
|
B2DCubicBezier aEdge;
|
|
sal_uInt32 nNewInd(0);
|
|
|
|
// add start point
|
|
aRetval.append(rCandidate.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 a(0); a < nCount; a++)
|
|
{
|
|
// get edge
|
|
rCandidate.getBezierSegment(a, aEdge);
|
|
|
|
if(aEdge.isBezier())
|
|
{
|
|
// control vectors involved for this edge
|
|
double fLeftStart(0.0);
|
|
|
|
// now add all points targeted to be at this index
|
|
while (nNewInd < nTempPointCount && rTempPoints[nNewInd].getIndex() == a && fLeftStart < 1.0)
|
|
{
|
|
const temporaryPoint& rTempPoint = rTempPoints[nNewInd++];
|
|
|
|
// split curve segment. Splits need to come sorted and need to be < 1.0. Also,
|
|
// since original segment is consumed from left to right, the cut values need
|
|
// to be scaled to the remaining part
|
|
B2DCubicBezier aLeftPart;
|
|
const double fRelativeSplitPoint((rTempPoint.getCut() - fLeftStart) / (1.0 - fLeftStart));
|
|
aEdge.split(fRelativeSplitPoint, &aLeftPart, &aEdge);
|
|
fLeftStart = rTempPoint.getCut();
|
|
|
|
// add left bow
|
|
aRetval.appendBezierSegment(aLeftPart.getControlPointA(), aLeftPart.getControlPointB(), rTempPoint.getPoint());
|
|
}
|
|
|
|
// add remaining bow
|
|
aRetval.appendBezierSegment(aEdge.getControlPointA(), aEdge.getControlPointB(), aEdge.getEndPoint());
|
|
}
|
|
else
|
|
{
|
|
// add all points targeted to be at this index
|
|
while(nNewInd < nTempPointCount && rTempPoints[nNewInd].getIndex() == a)
|
|
{
|
|
const temporaryPoint& rTempPoint = rTempPoints[nNewInd++];
|
|
const B2DPoint& aNewPoint(rTempPoint.getPoint());
|
|
|
|
// do not add points double
|
|
if(!aRetval.getB2DPoint(aRetval.count() - 1).equal(aNewPoint))
|
|
{
|
|
aRetval.append(aNewPoint);
|
|
}
|
|
}
|
|
|
|
// add edge end point
|
|
aRetval.append(aEdge.getEndPoint());
|
|
}
|
|
}
|
|
}
|
|
|
|
if(rCandidate.isClosed())
|
|
{
|
|
// set closed flag and correct last point (which is added double now).
|
|
utils::closeWithGeometryChange(aRetval);
|
|
}
|
|
|
|
return aRetval;
|
|
}
|
|
else
|
|
{
|
|
return rCandidate;
|
|
}
|
|
}
|
|
|
|
void adaptAndTransferCutsWithBezierSegment(
|
|
const temporaryPointVector& rPointVector, const B2DPolygon& rPolygon,
|
|
sal_uInt32 nInd, temporaryPointVector& rTempPoints)
|
|
{
|
|
// assuming that the subdivision to create rPolygon used equidistant pieces
|
|
// (as in adaptiveSubdivideByCount) it is now possible to calculate back the
|
|
// cut positions in the polygon to relative cut positions on the original bezier
|
|
// segment.
|
|
const sal_uInt32 nEdgeCount(rPolygon.count() ? rPolygon.count() - 1 : 0);
|
|
|
|
if(!rPointVector.empty() && nEdgeCount)
|
|
{
|
|
for( const auto& rTempPoint : rPointVector )
|
|
{
|
|
const double fCutPosInPolygon(static_cast<double>(rTempPoint.getIndex()) + rTempPoint.getCut());
|
|
const double fRelativeCutPos(fCutPosInPolygon / static_cast<double>(nEdgeCount));
|
|
rTempPoints.emplace_back(rTempPoint.getPoint(), nInd, fRelativeCutPos);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // end of anonymous namespace
|
|
} // end of namespace basegfx
|
|
|
|
namespace basegfx
|
|
{
|
|
namespace
|
|
{
|
|
|
|
// predefines for calls to this methods before method implementation
|
|
|
|
void findCuts(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints, size_t* pPointLimit = nullptr);
|
|
void findTouches(const B2DPolygon& rEdgePolygon, const B2DPolygon& rPointPolygon, temporaryPointVector& rTempPoints);
|
|
void findCuts(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB);
|
|
|
|
void findEdgeCutsTwoEdges(
|
|
const B2DPoint& rCurrA, const B2DPoint& rNextA,
|
|
const B2DPoint& rCurrB, const B2DPoint& rNextB,
|
|
sal_uInt32 nIndA, sal_uInt32 nIndB,
|
|
temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB)
|
|
{
|
|
// no null length edges
|
|
if(rCurrA.equal(rNextA) || rCurrB.equal(rNextB))
|
|
return;
|
|
|
|
// no common start/end points, this can be no cuts
|
|
if(rCurrB.equal(rCurrA) || rCurrB.equal(rNextA) || rNextB.equal(rCurrA) || rNextB.equal(rNextA))
|
|
return;
|
|
|
|
const B2DVector aVecA(rNextA - rCurrA);
|
|
const B2DVector aVecB(rNextB - rCurrB);
|
|
double fCut(aVecA.cross(aVecB));
|
|
|
|
if(fTools::equalZero(fCut))
|
|
return;
|
|
|
|
const double fZero(0.0);
|
|
const double fOne(1.0);
|
|
fCut = (aVecB.getY() * (rCurrB.getX() - rCurrA.getX()) + aVecB.getX() * (rCurrA.getY() - rCurrB.getY())) / fCut;
|
|
|
|
if (!fTools::betweenOrEqualEither(fCut, fZero, fOne))
|
|
return;
|
|
|
|
// it's a candidate, but also need to test parameter value of cut on line 2
|
|
double fCut2;
|
|
|
|
// choose the more precise version
|
|
if(fabs(aVecB.getX()) > fabs(aVecB.getY()))
|
|
{
|
|
fCut2 = (rCurrA.getX() + (fCut * aVecA.getX()) - rCurrB.getX()) / aVecB.getX();
|
|
}
|
|
else
|
|
{
|
|
fCut2 = (rCurrA.getY() + (fCut * aVecA.getY()) - rCurrB.getY()) / aVecB.getY();
|
|
}
|
|
|
|
if (fTools::betweenOrEqualEither(fCut2, fZero, fOne))
|
|
{
|
|
// cut is in range, add point. Two edges can have only one cut, but
|
|
// add a cut point to each list. The lists may be the same for
|
|
// self intersections.
|
|
const B2DPoint aCutPoint(interpolate(rCurrA, rNextA, fCut));
|
|
rTempPointsA.emplace_back(aCutPoint, nIndA, fCut);
|
|
rTempPointsB.emplace_back(aCutPoint, nIndB, fCut2);
|
|
}
|
|
}
|
|
|
|
void findCutsAndTouchesAndCommonForBezier(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB)
|
|
{
|
|
// #i76891#
|
|
// This new method is necessary since in findEdgeCutsBezierAndEdge and in findEdgeCutsTwoBeziers
|
|
// it is not sufficient to use findCuts() recursively. This will indeed find the cuts between the
|
|
// segments of the two temporarily adaptive subdivided bezier segments, but not the touches or
|
|
// equal points of them.
|
|
// It would be possible to find the touches using findTouches(), but at last with common points
|
|
// the adding of cut points (temporary points) would fail. But for these temporarily adaptive
|
|
// subdivided bezier segments, common points may be not very likely, but the bug shows that it
|
|
// happens.
|
|
// Touch points are a little bit more likely than common points. All in all it is best to use
|
|
// a specialized method here which can profit from knowing that it is working on a special
|
|
// family of B2DPolygons: no curve segments included and not closed.
|
|
OSL_ENSURE(!rCandidateA.areControlPointsUsed() && !rCandidateB.areControlPointsUsed(), "findCutsAndTouchesAndCommonForBezier only works with subdivided polygons (!)");
|
|
OSL_ENSURE(!rCandidateA.isClosed() && !rCandidateB.isClosed(), "findCutsAndTouchesAndCommonForBezier only works with opened polygons (!)");
|
|
const sal_uInt32 nPointCountA(rCandidateA.count());
|
|
const sal_uInt32 nPointCountB(rCandidateB.count());
|
|
|
|
if(nPointCountA <= 1 || nPointCountB <= 1)
|
|
return;
|
|
|
|
const sal_uInt32 nEdgeCountA(nPointCountA - 1);
|
|
const sal_uInt32 nEdgeCountB(nPointCountB - 1);
|
|
B2DPoint aCurrA(rCandidateA.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCountA; a++)
|
|
{
|
|
const B2DPoint aNextA(rCandidateA.getB2DPoint(a + 1));
|
|
const B2DRange aRangeA(aCurrA, aNextA);
|
|
B2DPoint aCurrB(rCandidateB.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 b(0); b < nEdgeCountB; b++)
|
|
{
|
|
const B2DPoint aNextB(rCandidateB.getB2DPoint(b + 1));
|
|
const B2DRange aRangeB(aCurrB, aNextB);
|
|
|
|
if(aRangeA.overlaps(aRangeB))
|
|
{
|
|
// no null length edges
|
|
if(!(aCurrA.equal(aNextA) || aCurrB.equal(aNextB)))
|
|
{
|
|
const B2DVector aVecA(aNextA - aCurrA);
|
|
const B2DVector aVecB(aNextB - aCurrB);
|
|
double fCutA(aVecA.cross(aVecB));
|
|
|
|
if(!fTools::equalZero(fCutA))
|
|
{
|
|
const double fZero(0.0);
|
|
const double fOne(1.0);
|
|
fCutA = (aVecB.getY() * (aCurrB.getX() - aCurrA.getX()) + aVecB.getX() * (aCurrA.getY() - aCurrB.getY())) / fCutA;
|
|
|
|
// use range [0.0 .. 1.0[, thus in the loop, all direct aCurrA cuts will be registered
|
|
// as 0.0 cut. The 1.0 cut will be registered in the next loop step
|
|
if(fTools::moreOrEqual(fCutA, fZero) && fTools::less(fCutA, fOne))
|
|
{
|
|
// it's a candidate, but also need to test parameter value of cut on line 2
|
|
double fCutB;
|
|
|
|
// choose the more precise version
|
|
if(fabs(aVecB.getX()) > fabs(aVecB.getY()))
|
|
{
|
|
fCutB = (aCurrA.getX() + (fCutA * aVecA.getX()) - aCurrB.getX()) / aVecB.getX();
|
|
}
|
|
else
|
|
{
|
|
fCutB = (aCurrA.getY() + (fCutA * aVecA.getY()) - aCurrB.getY()) / aVecB.getY();
|
|
}
|
|
|
|
// use range [0.0 .. 1.0[, thus in the loop, all direct aCurrA cuts will be registered
|
|
// as 0.0 cut. The 1.0 cut will be registered in the next loop step
|
|
if(fTools::moreOrEqual(fCutB, fZero) && fTools::less(fCutB, fOne))
|
|
{
|
|
// cut is in both ranges. Add points for A and B
|
|
// #i111715# use fTools::equal instead of fTools::equalZero for better accuracy
|
|
if(fTools::equal(fCutA, fZero))
|
|
{
|
|
// ignore for start point in first edge; this is handled
|
|
// by outer methods and would just produce a double point
|
|
if(a)
|
|
{
|
|
rTempPointsA.emplace_back(aCurrA, a, 0.0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const B2DPoint aCutPoint(interpolate(aCurrA, aNextA, fCutA));
|
|
rTempPointsA.emplace_back(aCutPoint, a, fCutA);
|
|
}
|
|
|
|
// #i111715# use fTools::equal instead of fTools::equalZero for better accuracy
|
|
if(fTools::equal(fCutB, fZero))
|
|
{
|
|
// ignore for start point in first edge; this is handled
|
|
// by outer methods and would just produce a double point
|
|
if(b)
|
|
{
|
|
rTempPointsB.emplace_back(aCurrB, b, 0.0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const B2DPoint aCutPoint(interpolate(aCurrB, aNextB, fCutB));
|
|
rTempPointsB.emplace_back(aCutPoint, b, fCutB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// prepare next step
|
|
aCurrB = aNextB;
|
|
}
|
|
|
|
// prepare next step
|
|
aCurrA = aNextA;
|
|
}
|
|
}
|
|
|
|
void findEdgeCutsBezierAndEdge(
|
|
const B2DCubicBezier& rCubicA,
|
|
const B2DPoint& rCurrB, const B2DPoint& rNextB,
|
|
sal_uInt32 nIndA, sal_uInt32 nIndB,
|
|
temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB)
|
|
{
|
|
// find all cuts between given bezier segment and edge. Add an entry to the tempPoints
|
|
// for each common point with the cut value describing the relative position on given
|
|
// bezier segment and edge.
|
|
B2DPolygon aTempPolygonA;
|
|
B2DPolygon aTempPolygonEdge;
|
|
temporaryPointVector aTempPointVectorA;
|
|
temporaryPointVector aTempPointVectorEdge;
|
|
|
|
// create subdivided polygons and find cuts between them
|
|
// Keep adaptiveSubdivideByCount due to needed quality
|
|
aTempPolygonA.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8);
|
|
aTempPolygonA.append(rCubicA.getStartPoint());
|
|
rCubicA.adaptiveSubdivideByCount(aTempPolygonA, SUBDIVIDE_FOR_CUT_TEST_COUNT);
|
|
aTempPolygonEdge.append(rCurrB);
|
|
aTempPolygonEdge.append(rNextB);
|
|
|
|
// #i76891# using findCuts recursively is not sufficient here
|
|
findCutsAndTouchesAndCommonForBezier(aTempPolygonA, aTempPolygonEdge, aTempPointVectorA, aTempPointVectorEdge);
|
|
|
|
if(!aTempPointVectorA.empty())
|
|
{
|
|
// adapt tempVector entries to segment
|
|
adaptAndTransferCutsWithBezierSegment(aTempPointVectorA, aTempPolygonA, nIndA, rTempPointsA);
|
|
}
|
|
|
|
// append remapped tempVector entries for edge to tempPoints for edge
|
|
for(const temporaryPoint & rTempPoint : aTempPointVectorEdge)
|
|
{
|
|
rTempPointsB.emplace_back(rTempPoint.getPoint(), nIndB, rTempPoint.getCut());
|
|
}
|
|
}
|
|
|
|
void findEdgeCutsTwoBeziers(
|
|
const B2DCubicBezier& rCubicA,
|
|
const B2DCubicBezier& rCubicB,
|
|
sal_uInt32 nIndA, sal_uInt32 nIndB,
|
|
temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB)
|
|
{
|
|
// find all cuts between the two given bezier segments. Add an entry to the tempPoints
|
|
// for each common point with the cut value describing the relative position on given
|
|
// bezier segments.
|
|
B2DPolygon aTempPolygonA;
|
|
B2DPolygon aTempPolygonB;
|
|
temporaryPointVector aTempPointVectorA;
|
|
temporaryPointVector aTempPointVectorB;
|
|
|
|
// create subdivided polygons and find cuts between them
|
|
// Keep adaptiveSubdivideByCount due to needed quality
|
|
aTempPolygonA.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8);
|
|
aTempPolygonA.append(rCubicA.getStartPoint());
|
|
rCubicA.adaptiveSubdivideByCount(aTempPolygonA, SUBDIVIDE_FOR_CUT_TEST_COUNT);
|
|
aTempPolygonB.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8);
|
|
aTempPolygonB.append(rCubicB.getStartPoint());
|
|
rCubicB.adaptiveSubdivideByCount(aTempPolygonB, SUBDIVIDE_FOR_CUT_TEST_COUNT);
|
|
|
|
// #i76891# using findCuts recursively is not sufficient here
|
|
findCutsAndTouchesAndCommonForBezier(aTempPolygonA, aTempPolygonB, aTempPointVectorA, aTempPointVectorB);
|
|
|
|
if(!aTempPointVectorA.empty())
|
|
{
|
|
// adapt tempVector entries to segment
|
|
adaptAndTransferCutsWithBezierSegment(aTempPointVectorA, aTempPolygonA, nIndA, rTempPointsA);
|
|
}
|
|
|
|
if(!aTempPointVectorB.empty())
|
|
{
|
|
// adapt tempVector entries to segment
|
|
adaptAndTransferCutsWithBezierSegment(aTempPointVectorB, aTempPolygonB, nIndB, rTempPointsB);
|
|
}
|
|
}
|
|
|
|
void findEdgeCutsOneBezier(
|
|
const B2DCubicBezier& rCubicA,
|
|
sal_uInt32 nInd, temporaryPointVector& rTempPoints)
|
|
{
|
|
// avoid expensive part of this method if possible
|
|
// TODO: use hasAnyExtremum() method instead when it becomes available
|
|
double fDummy;
|
|
const bool bHasAnyExtremum = rCubicA.getMinimumExtremumPosition( fDummy );
|
|
if( !bHasAnyExtremum )
|
|
return;
|
|
|
|
// find all self-intersections on the given bezier segment. Add an entry to the tempPoints
|
|
// for each self intersection point with the cut value describing the relative position on given
|
|
// bezier segment.
|
|
B2DPolygon aTempPolygon;
|
|
temporaryPointVector aTempPointVector;
|
|
|
|
// create subdivided polygon and find cuts on it
|
|
// Keep adaptiveSubdivideByCount due to needed quality
|
|
aTempPolygon.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8);
|
|
aTempPolygon.append(rCubicA.getStartPoint());
|
|
rCubicA.adaptiveSubdivideByCount(aTempPolygon, SUBDIVIDE_FOR_CUT_TEST_COUNT);
|
|
findCuts(aTempPolygon, aTempPointVector);
|
|
|
|
if(!aTempPointVector.empty())
|
|
{
|
|
// adapt tempVector entries to segment
|
|
adaptAndTransferCutsWithBezierSegment(aTempPointVector, aTempPolygon, nInd, rTempPoints);
|
|
}
|
|
}
|
|
|
|
void findCuts(const B2DPolygon& rCandidate, temporaryPointVector& rTempPoints, size_t* pPointLimit)
|
|
{
|
|
// find out if there are edges with intersections (self-cuts). If yes, add
|
|
// entries to rTempPoints accordingly
|
|
const sal_uInt32 nPointCount(rCandidate.count());
|
|
|
|
if(!nPointCount)
|
|
return;
|
|
|
|
const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1);
|
|
|
|
if(!nEdgeCount)
|
|
return;
|
|
|
|
const bool bCurvesInvolved(rCandidate.areControlPointsUsed());
|
|
|
|
if(bCurvesInvolved)
|
|
{
|
|
B2DCubicBezier aCubicA;
|
|
B2DCubicBezier aCubicB;
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount - 1; a++)
|
|
{
|
|
rCandidate.getBezierSegment(a, aCubicA);
|
|
aCubicA.testAndSolveTrivialBezier();
|
|
const bool bEdgeAIsCurve(aCubicA.isBezier());
|
|
const B2DRange aRangeA(aCubicA.getRange());
|
|
|
|
if(bEdgeAIsCurve)
|
|
{
|
|
// curved segments may have self-intersections, do not forget those (!)
|
|
findEdgeCutsOneBezier(aCubicA, a, rTempPoints);
|
|
}
|
|
|
|
for(sal_uInt32 b(a + 1); b < nEdgeCount; b++)
|
|
{
|
|
rCandidate.getBezierSegment(b, aCubicB);
|
|
aCubicB.testAndSolveTrivialBezier();
|
|
const B2DRange aRangeB(aCubicB.getRange());
|
|
|
|
// only overlapping segments need to be tested
|
|
// consecutive segments touch of course
|
|
bool bOverlap = false;
|
|
if( b > a+1)
|
|
bOverlap = aRangeA.overlaps(aRangeB);
|
|
else
|
|
bOverlap = aRangeA.overlapsMore(aRangeB);
|
|
if( bOverlap)
|
|
{
|
|
const bool bEdgeBIsCurve(aCubicB.isBezier());
|
|
if(bEdgeAIsCurve && bEdgeBIsCurve)
|
|
{
|
|
// test for bezier-bezier cuts
|
|
findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, rTempPoints, rTempPoints);
|
|
}
|
|
else if(bEdgeAIsCurve)
|
|
{
|
|
// test for bezier-edge cuts
|
|
findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, rTempPoints, rTempPoints);
|
|
}
|
|
else if(bEdgeBIsCurve)
|
|
{
|
|
// test for bezier-edge cuts
|
|
findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, rTempPoints, rTempPoints);
|
|
}
|
|
else
|
|
{
|
|
// test for simple edge-edge cuts
|
|
findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(),
|
|
a, b, rTempPoints, rTempPoints);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
B2DPoint aCurrA(rCandidate.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount - 1; a++)
|
|
{
|
|
const B2DPoint aNextA(rCandidate.getB2DPoint(a + 1 == nPointCount ? 0 : a + 1));
|
|
const B2DRange aRangeA(aCurrA, aNextA);
|
|
B2DPoint aCurrB(rCandidate.getB2DPoint(a + 1));
|
|
|
|
for(sal_uInt32 b(a + 1); b < nEdgeCount; b++)
|
|
{
|
|
const B2DPoint aNextB(rCandidate.getB2DPoint(b + 1 == nPointCount ? 0 : b + 1));
|
|
const B2DRange aRangeB(aCurrB, aNextB);
|
|
|
|
// consecutive segments touch of course
|
|
bool bOverlap = false;
|
|
if( b > a+1)
|
|
bOverlap = aRangeA.overlaps(aRangeB);
|
|
else
|
|
bOverlap = aRangeA.overlapsMore(aRangeB);
|
|
if( bOverlap)
|
|
{
|
|
findEdgeCutsTwoEdges(aCurrA, aNextA, aCurrB, aNextB, a, b, rTempPoints, rTempPoints);
|
|
}
|
|
|
|
if (pPointLimit && rTempPoints.size() > *pPointLimit)
|
|
break;
|
|
|
|
// prepare next step
|
|
aCurrB = aNextB;
|
|
}
|
|
|
|
// prepare next step
|
|
aCurrA = aNextA;
|
|
}
|
|
}
|
|
|
|
if (pPointLimit)
|
|
{
|
|
if (rTempPoints.size() > *pPointLimit)
|
|
*pPointLimit = 0;
|
|
else
|
|
*pPointLimit -= rTempPoints.size();
|
|
}
|
|
}
|
|
|
|
} // end of anonymous namespace
|
|
} // end of namespace basegfx
|
|
|
|
namespace basegfx
|
|
{
|
|
namespace
|
|
{
|
|
|
|
void findTouchesOnEdge(
|
|
const B2DPoint& rCurr, const B2DPoint& rNext, const B2DPolygon& rPointPolygon,
|
|
sal_uInt32 nInd, temporaryPointVector& rTempPoints)
|
|
{
|
|
// find out if points from rPointPolygon are positioned on given edge. If Yes, add
|
|
// points there to represent touches (which may be enter or leave nodes later).
|
|
const sal_uInt32 nPointCount(rPointPolygon.count());
|
|
|
|
if(!nPointCount)
|
|
return;
|
|
|
|
const B2DRange aRange(rCurr, rNext);
|
|
const B2DVector aEdgeVector(rNext - rCurr);
|
|
bool bTestUsingX(fabs(aEdgeVector.getX()) > fabs(aEdgeVector.getY()));
|
|
|
|
for(sal_uInt32 a(0); a < nPointCount; a++)
|
|
{
|
|
const B2DPoint aTestPoint(rPointPolygon.getB2DPoint(a));
|
|
|
|
if(aRange.isInside(aTestPoint))
|
|
{
|
|
if(!aTestPoint.equal(rCurr) && !aTestPoint.equal(rNext))
|
|
{
|
|
const B2DVector aTestVector(aTestPoint - rCurr);
|
|
|
|
if(areParallel(aEdgeVector, aTestVector))
|
|
{
|
|
const double fCut(bTestUsingX
|
|
? aTestVector.getX() / aEdgeVector.getX()
|
|
: aTestVector.getY() / aEdgeVector.getY());
|
|
const double fZero(0.0);
|
|
const double fOne(1.0);
|
|
|
|
if(fTools::more(fCut, fZero) && fTools::less(fCut, fOne))
|
|
{
|
|
rTempPoints.emplace_back(aTestPoint, nInd, fCut);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void findTouchesOnCurve(
|
|
const B2DCubicBezier& rCubicA, const B2DPolygon& rPointPolygon,
|
|
sal_uInt32 nInd, temporaryPointVector& rTempPoints)
|
|
{
|
|
// find all points from rPointPolygon which touch the given bezier segment. Add an entry
|
|
// for each touch to the given pointVector. The cut for that entry is the relative position on
|
|
// the given bezier segment.
|
|
B2DPolygon aTempPolygon;
|
|
temporaryPointVector aTempPointVector;
|
|
|
|
// create subdivided polygon and find cuts on it
|
|
// Keep adaptiveSubdivideByCount due to needed quality
|
|
aTempPolygon.reserve(SUBDIVIDE_FOR_CUT_TEST_COUNT + 8);
|
|
aTempPolygon.append(rCubicA.getStartPoint());
|
|
rCubicA.adaptiveSubdivideByCount(aTempPolygon, SUBDIVIDE_FOR_CUT_TEST_COUNT);
|
|
findTouches(aTempPolygon, rPointPolygon, aTempPointVector);
|
|
|
|
if(!aTempPointVector.empty())
|
|
{
|
|
// adapt tempVector entries to segment
|
|
adaptAndTransferCutsWithBezierSegment(aTempPointVector, aTempPolygon, nInd, rTempPoints);
|
|
}
|
|
}
|
|
|
|
void findTouches(const B2DPolygon& rEdgePolygon, const B2DPolygon& rPointPolygon, temporaryPointVector& rTempPoints)
|
|
{
|
|
// find out if points from rPointPolygon touch edges from rEdgePolygon. If yes,
|
|
// add entries to rTempPoints
|
|
const sal_uInt32 nPointCount(rPointPolygon.count());
|
|
const sal_uInt32 nEdgePointCount(rEdgePolygon.count());
|
|
|
|
if(!(nPointCount && nEdgePointCount))
|
|
return;
|
|
|
|
const sal_uInt32 nEdgeCount(rEdgePolygon.isClosed() ? nEdgePointCount : nEdgePointCount - 1);
|
|
B2DPoint aCurr(rEdgePolygon.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount; a++)
|
|
{
|
|
const sal_uInt32 nNextIndex((a + 1) % nEdgePointCount);
|
|
const B2DPoint aNext(rEdgePolygon.getB2DPoint(nNextIndex));
|
|
|
|
if(!aCurr.equal(aNext))
|
|
{
|
|
bool bHandleAsSimpleEdge(true);
|
|
|
|
if(rEdgePolygon.areControlPointsUsed())
|
|
{
|
|
const B2DPoint aNextControlPoint(rEdgePolygon.getNextControlPoint(a));
|
|
const B2DPoint aPrevControlPoint(rEdgePolygon.getPrevControlPoint(nNextIndex));
|
|
const bool bEdgeIsCurve(!aNextControlPoint.equal(aCurr) || !aPrevControlPoint.equal(aNext));
|
|
|
|
if(bEdgeIsCurve)
|
|
{
|
|
bHandleAsSimpleEdge = false;
|
|
const B2DCubicBezier aCubicA(aCurr, aNextControlPoint, aPrevControlPoint, aNext);
|
|
findTouchesOnCurve(aCubicA, rPointPolygon, a, rTempPoints);
|
|
}
|
|
}
|
|
|
|
if(bHandleAsSimpleEdge)
|
|
{
|
|
findTouchesOnEdge(aCurr, aNext, rPointPolygon, a, rTempPoints);
|
|
}
|
|
}
|
|
|
|
// next step
|
|
aCurr = aNext;
|
|
}
|
|
}
|
|
|
|
} // end of anonymous namespace
|
|
} // end of namespace basegfx
|
|
|
|
namespace basegfx
|
|
{
|
|
namespace
|
|
{
|
|
|
|
void findCuts(const B2DPolygon& rCandidateA, const B2DPolygon& rCandidateB, temporaryPointVector& rTempPointsA, temporaryPointVector& rTempPointsB)
|
|
{
|
|
// find out if edges from both polygons cut. If so, add entries to rTempPoints which
|
|
// should be added to the polygons accordingly
|
|
const sal_uInt32 nPointCountA(rCandidateA.count());
|
|
const sal_uInt32 nPointCountB(rCandidateB.count());
|
|
|
|
if(!(nPointCountA && nPointCountB))
|
|
return;
|
|
|
|
const sal_uInt32 nEdgeCountA(rCandidateA.isClosed() ? nPointCountA : nPointCountA - 1);
|
|
const sal_uInt32 nEdgeCountB(rCandidateB.isClosed() ? nPointCountB : nPointCountB - 1);
|
|
|
|
if(!(nEdgeCountA && nEdgeCountB))
|
|
return;
|
|
|
|
const bool bCurvesInvolved(rCandidateA.areControlPointsUsed() || rCandidateB.areControlPointsUsed());
|
|
|
|
if(bCurvesInvolved)
|
|
{
|
|
B2DCubicBezier aCubicA;
|
|
B2DCubicBezier aCubicB;
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCountA; a++)
|
|
{
|
|
rCandidateA.getBezierSegment(a, aCubicA);
|
|
aCubicA.testAndSolveTrivialBezier();
|
|
const bool bEdgeAIsCurve(aCubicA.isBezier());
|
|
const B2DRange aRangeA(aCubicA.getRange());
|
|
|
|
for(sal_uInt32 b(0); b < nEdgeCountB; b++)
|
|
{
|
|
rCandidateB.getBezierSegment(b, aCubicB);
|
|
aCubicB.testAndSolveTrivialBezier();
|
|
const B2DRange aRangeB(aCubicB.getRange());
|
|
|
|
// consecutive segments touch of course
|
|
bool bOverlap = false;
|
|
if( b > a+1)
|
|
bOverlap = aRangeA.overlaps(aRangeB);
|
|
else
|
|
bOverlap = aRangeA.overlapsMore(aRangeB);
|
|
if( bOverlap)
|
|
{
|
|
const bool bEdgeBIsCurve(aCubicB.isBezier());
|
|
if(bEdgeAIsCurve && bEdgeBIsCurve)
|
|
{
|
|
// test for bezier-bezier cuts
|
|
findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, rTempPointsA, rTempPointsB);
|
|
}
|
|
else if(bEdgeAIsCurve)
|
|
{
|
|
// test for bezier-edge cuts
|
|
findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, rTempPointsA, rTempPointsB);
|
|
}
|
|
else if(bEdgeBIsCurve)
|
|
{
|
|
// test for bezier-edge cuts
|
|
findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, rTempPointsB, rTempPointsA);
|
|
}
|
|
else
|
|
{
|
|
// test for simple edge-edge cuts
|
|
findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(),
|
|
a, b, rTempPointsA, rTempPointsB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
B2DPoint aCurrA(rCandidateA.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCountA; a++)
|
|
{
|
|
const B2DPoint aNextA(rCandidateA.getB2DPoint(a + 1 == nPointCountA ? 0 : a + 1));
|
|
const B2DRange aRangeA(aCurrA, aNextA);
|
|
B2DPoint aCurrB(rCandidateB.getB2DPoint(0));
|
|
|
|
for(sal_uInt32 b(0); b < nEdgeCountB; b++)
|
|
{
|
|
const B2DPoint aNextB(rCandidateB.getB2DPoint(b + 1 == nPointCountB ? 0 : b + 1));
|
|
const B2DRange aRangeB(aCurrB, aNextB);
|
|
|
|
// consecutive segments touch of course
|
|
bool bOverlap = false;
|
|
if( b > a+1)
|
|
bOverlap = aRangeA.overlaps(aRangeB);
|
|
else
|
|
bOverlap = aRangeA.overlapsMore(aRangeB);
|
|
if( bOverlap)
|
|
{
|
|
// test for simple edge-edge cuts
|
|
findEdgeCutsTwoEdges(aCurrA, aNextA, aCurrB, aNextB, a, b, rTempPointsA, rTempPointsB);
|
|
}
|
|
|
|
// prepare next step
|
|
aCurrB = aNextB;
|
|
}
|
|
|
|
// prepare next step
|
|
aCurrA = aNextA;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // end of anonymous namespace
|
|
} // end of namespace basegfx
|
|
|
|
namespace basegfx::utils
|
|
{
|
|
|
|
B2DPolygon addPointsAtCutsAndTouches(const B2DPolygon& rCandidate, size_t* pPointLimit)
|
|
{
|
|
if(rCandidate.count())
|
|
{
|
|
temporaryPointVector aTempPoints;
|
|
|
|
findTouches(rCandidate, rCandidate, aTempPoints);
|
|
findCuts(rCandidate, aTempPoints, pPointLimit);
|
|
if (pPointLimit && !*pPointLimit)
|
|
{
|
|
SAL_WARN("basegfx", "addPointsAtCutsAndTouches hit point limit");
|
|
return rCandidate;
|
|
}
|
|
|
|
return mergeTemporaryPointsAndPolygon(rCandidate, aTempPoints);
|
|
}
|
|
else
|
|
{
|
|
return rCandidate;
|
|
}
|
|
}
|
|
|
|
B2DPolyPolygon addPointsAtCutsAndTouches(const B2DPolyPolygon& rCandidate, size_t* pPointLimit)
|
|
{
|
|
const sal_uInt32 nCount(rCandidate.count());
|
|
|
|
if(nCount)
|
|
{
|
|
B2DPolyPolygon aRetval;
|
|
|
|
if(nCount == 1)
|
|
{
|
|
// remove self intersections
|
|
aRetval.append(addPointsAtCutsAndTouches(rCandidate.getB2DPolygon(0), pPointLimit));
|
|
}
|
|
else
|
|
{
|
|
// first solve self cuts and self touches for all contained single polygons
|
|
std::unique_ptr<temporaryPolygonData[]> pTempData(new temporaryPolygonData[nCount]);
|
|
sal_uInt32 a, b;
|
|
|
|
for(a = 0; a < nCount; a++)
|
|
{
|
|
// use polygons with solved self intersections
|
|
pTempData[a].setPolygon(addPointsAtCutsAndTouches(rCandidate.getB2DPolygon(a), pPointLimit));
|
|
}
|
|
|
|
if (pPointLimit && !*pPointLimit)
|
|
{
|
|
SAL_WARN("basegfx", "addPointsAtCutsAndTouches hit point limit");
|
|
return rCandidate;
|
|
}
|
|
|
|
// now cuts and touches between the polygons
|
|
for(a = 0; a < nCount; a++)
|
|
{
|
|
for(b = 0; b < nCount; b++)
|
|
{
|
|
if(a != b)
|
|
{
|
|
// look for touches, compare each edge polygon to all other points
|
|
if(pTempData[a].getRange().overlaps(pTempData[b].getRange()))
|
|
{
|
|
findTouches(pTempData[a].getPolygon(), pTempData[b].getPolygon(), pTempData[a].getTemporaryPointVector());
|
|
}
|
|
}
|
|
|
|
if(a < b)
|
|
{
|
|
// look for cuts, compare each edge polygon to following ones
|
|
if(pTempData[a].getRange().overlaps(pTempData[b].getRange()))
|
|
{
|
|
findCuts(pTempData[a].getPolygon(), pTempData[b].getPolygon(), pTempData[a].getTemporaryPointVector(), pTempData[b].getTemporaryPointVector());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// consolidate the result
|
|
for(a = 0; a < nCount; a++)
|
|
{
|
|
aRetval.append(mergeTemporaryPointsAndPolygon(pTempData[a].getPolygon(), pTempData[a].getTemporaryPointVector()));
|
|
}
|
|
}
|
|
|
|
return aRetval;
|
|
}
|
|
else
|
|
{
|
|
return rCandidate;
|
|
}
|
|
}
|
|
|
|
B2DPolygon addPointsAtCuts(const B2DPolygon& rCandidate, const B2DPoint& rStart, const B2DPoint& rEnd)
|
|
{
|
|
const sal_uInt32 nCount(rCandidate.count());
|
|
|
|
if(nCount && !rStart.equal(rEnd))
|
|
{
|
|
const B2DRange aPolygonRange(rCandidate.getB2DRange());
|
|
const B2DRange aEdgeRange(rStart, rEnd);
|
|
|
|
if(aPolygonRange.overlaps(aEdgeRange))
|
|
{
|
|
const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nCount : nCount - 1);
|
|
temporaryPointVector aTempPoints;
|
|
temporaryPointVector aUnusedTempPoints;
|
|
B2DCubicBezier aCubic;
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCount; a++)
|
|
{
|
|
rCandidate.getBezierSegment(a, aCubic);
|
|
B2DRange aCubicRange(aCubic.getStartPoint(), aCubic.getEndPoint());
|
|
|
|
if(aCubic.isBezier())
|
|
{
|
|
aCubicRange.expand(aCubic.getControlPointA());
|
|
aCubicRange.expand(aCubic.getControlPointB());
|
|
|
|
if(aCubicRange.overlaps(aEdgeRange))
|
|
{
|
|
findEdgeCutsBezierAndEdge(aCubic, rStart, rEnd, a, 0, aTempPoints, aUnusedTempPoints);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(aCubicRange.overlaps(aEdgeRange))
|
|
{
|
|
findEdgeCutsTwoEdges(aCubic.getStartPoint(), aCubic.getEndPoint(), rStart, rEnd, a, 0, aTempPoints, aUnusedTempPoints);
|
|
}
|
|
}
|
|
}
|
|
|
|
return mergeTemporaryPointsAndPolygon(rCandidate, aTempPoints);
|
|
}
|
|
}
|
|
|
|
return rCandidate;
|
|
}
|
|
|
|
B2DPolygon addPointsAtCuts(const B2DPolygon& rCandidate, const B2DPolyPolygon& rPolyMask)
|
|
{
|
|
const sal_uInt32 nCountA(rCandidate.count());
|
|
const sal_uInt32 nCountM(rPolyMask.count());
|
|
|
|
if(nCountA && nCountM)
|
|
{
|
|
const B2DRange aRangeA(rCandidate.getB2DRange());
|
|
const B2DRange aRangeM(rPolyMask.getB2DRange());
|
|
|
|
if(aRangeA.overlaps(aRangeM))
|
|
{
|
|
const sal_uInt32 nEdgeCountA(rCandidate.isClosed() ? nCountA : nCountA - 1);
|
|
temporaryPointVector aTempPointsA;
|
|
temporaryPointVector aUnusedTempPointsB;
|
|
|
|
for(sal_uInt32 m(0); m < nCountM; m++)
|
|
{
|
|
const B2DPolygon& aMask(rPolyMask.getB2DPolygon(m));
|
|
const sal_uInt32 nCountB(aMask.count());
|
|
|
|
if(nCountB)
|
|
{
|
|
B2DCubicBezier aCubicA;
|
|
B2DCubicBezier aCubicB;
|
|
|
|
for(sal_uInt32 a(0); a < nEdgeCountA; a++)
|
|
{
|
|
rCandidate.getBezierSegment(a, aCubicA);
|
|
const bool bCubicAIsCurve(aCubicA.isBezier());
|
|
B2DRange aCubicRangeA(aCubicA.getStartPoint(), aCubicA.getEndPoint());
|
|
|
|
if(bCubicAIsCurve)
|
|
{
|
|
aCubicRangeA.expand(aCubicA.getControlPointA());
|
|
aCubicRangeA.expand(aCubicA.getControlPointB());
|
|
}
|
|
|
|
for(sal_uInt32 b(0); b < nCountB; b++)
|
|
{
|
|
aMask.getBezierSegment(b, aCubicB);
|
|
const bool bCubicBIsCurve(aCubicB.isBezier());
|
|
B2DRange aCubicRangeB(aCubicB.getStartPoint(), aCubicB.getEndPoint());
|
|
|
|
if(bCubicBIsCurve)
|
|
{
|
|
aCubicRangeB.expand(aCubicB.getControlPointA());
|
|
aCubicRangeB.expand(aCubicB.getControlPointB());
|
|
}
|
|
|
|
if(aCubicRangeA.overlaps(aCubicRangeB))
|
|
{
|
|
if(bCubicAIsCurve && bCubicBIsCurve)
|
|
{
|
|
findEdgeCutsTwoBeziers(aCubicA, aCubicB, a, b, aTempPointsA, aUnusedTempPointsB);
|
|
}
|
|
else if(bCubicAIsCurve)
|
|
{
|
|
findEdgeCutsBezierAndEdge(aCubicA, aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, aTempPointsA, aUnusedTempPointsB);
|
|
}
|
|
else if(bCubicBIsCurve)
|
|
{
|
|
findEdgeCutsBezierAndEdge(aCubicB, aCubicA.getStartPoint(), aCubicA.getEndPoint(), b, a, aUnusedTempPointsB, aTempPointsA);
|
|
}
|
|
else
|
|
{
|
|
findEdgeCutsTwoEdges(aCubicA.getStartPoint(), aCubicA.getEndPoint(), aCubicB.getStartPoint(), aCubicB.getEndPoint(), a, b, aTempPointsA, aUnusedTempPointsB);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return mergeTemporaryPointsAndPolygon(rCandidate, aTempPointsA);
|
|
}
|
|
}
|
|
|
|
return rCandidate;
|
|
}
|
|
|
|
} // end of namespace
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|