/src/libreoffice/basegfx/source/polygon/b2dlinegeometry.cxx
Line | Count | Source (jump to first uncovered line) |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <osl/diagnose.h> |
21 | | #include <sal/log.hxx> |
22 | | #include <basegfx/polygon/b2dlinegeometry.hxx> |
23 | | #include <basegfx/point/b2dpoint.hxx> |
24 | | #include <basegfx/vector/b2dvector.hxx> |
25 | | #include <basegfx/polygon/b2dpolygontools.hxx> |
26 | | #include <basegfx/polygon/b2dpolypolygontools.hxx> |
27 | | #include <basegfx/range/b2drange.hxx> |
28 | | #include <basegfx/matrix/b2dhommatrix.hxx> |
29 | | #include <basegfx/curve/b2dcubicbezier.hxx> |
30 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
31 | | #include <com/sun/star/drawing/LineCap.hpp> |
32 | | #include <basegfx/polygon/b2dpolypolygoncutter.hxx> |
33 | | #include <basegfx/polygon/b2dpolygontriangulator.hxx> |
34 | | |
35 | | namespace basegfx::utils |
36 | | { |
37 | | B2DPolyPolygon createAreaGeometryForLineStartEnd( |
38 | | const B2DPolygon& rCandidate, |
39 | | const B2DPolyPolygon& rArrow, |
40 | | bool bStart, |
41 | | double fWidth, |
42 | | double fCandidateLength, |
43 | | double fDockingPosition, // 0->top, 1->bottom |
44 | | double* pConsumedLength, |
45 | | double fShift) |
46 | 0 | { |
47 | 0 | B2DPolyPolygon aRetval; |
48 | 0 | assert((rCandidate.count() > 1) && "createAreaGeometryForLineStartEnd: Line polygon has too few points"); |
49 | 0 | assert((rArrow.count() > 0) && "createAreaGeometryForLineStartEnd: Empty arrow utils::PolyPolygon"); |
50 | 0 | assert((fWidth > 0.0) && "createAreaGeometryForLineStartEnd: Width too small"); |
51 | 0 | assert((fDockingPosition >= 0.0 && fDockingPosition <= 1.0) && |
52 | 0 | "createAreaGeometryForLineStartEnd: fDockingPosition out of range [0.0 .. 1.0]"); |
53 | |
|
54 | 0 | if(fWidth < 0.0) |
55 | 0 | { |
56 | 0 | fWidth = -fWidth; |
57 | 0 | } |
58 | |
|
59 | 0 | if(rCandidate.count() > 1 && rArrow.count() && !fTools::equalZero(fWidth)) |
60 | 0 | { |
61 | 0 | if(fDockingPosition < 0.0) |
62 | 0 | { |
63 | 0 | fDockingPosition = 0.0; |
64 | 0 | } |
65 | 0 | else if(fDockingPosition > 1.0) |
66 | 0 | { |
67 | 0 | fDockingPosition = 1.0; |
68 | 0 | } |
69 | | |
70 | | // init return value from arrow |
71 | 0 | aRetval.append(rArrow); |
72 | | |
73 | | // get size of the arrow |
74 | 0 | const B2DRange aArrowSize(getRange(rArrow)); |
75 | | |
76 | | // build ArrowTransform; center in X, align with axis in Y |
77 | 0 | B2DHomMatrix aArrowTransform(basegfx::utils::createTranslateB2DHomMatrix( |
78 | 0 | -aArrowSize.getCenter().getX(), -aArrowSize.getMinimum().getY())); |
79 | | |
80 | | // scale to target size |
81 | 0 | const double fArrowScale(fWidth / (aArrowSize.getWidth())); |
82 | 0 | aArrowTransform.scale(fArrowScale, fArrowScale); |
83 | | |
84 | | // get arrow size in Y |
85 | 0 | B2DPoint aUpperCenter(aArrowSize.getCenter().getX(), aArrowSize.getMaximum().getY()); |
86 | 0 | aUpperCenter *= aArrowTransform; |
87 | 0 | const double fArrowYLength(B2DVector(aUpperCenter).getLength()); |
88 | | |
89 | | // move arrow to have docking position centered |
90 | 0 | aArrowTransform.translate(0.0, -fArrowYLength * fDockingPosition + fShift); |
91 | | |
92 | | // prepare polygon length |
93 | 0 | if(fTools::equalZero(fCandidateLength)) |
94 | 0 | { |
95 | 0 | fCandidateLength = getLength(rCandidate); |
96 | 0 | } |
97 | | |
98 | | // get the polygon vector we want to plant this arrow on |
99 | 0 | const double fConsumedLength(fArrowYLength * (1.0 - fDockingPosition) - fShift); |
100 | 0 | const B2DVector aHead(rCandidate.getB2DPoint(bStart ? 0 : rCandidate.count() - 1)); |
101 | 0 | const B2DVector aTail(getPositionAbsolute(rCandidate, |
102 | 0 | bStart ? fConsumedLength : fCandidateLength - fConsumedLength, fCandidateLength)); |
103 | | |
104 | | // from that vector, take the needed rotation and add rotate for arrow to transformation |
105 | 0 | const B2DVector aTargetDirection(aHead - aTail); |
106 | 0 | const double fRotation(atan2(aTargetDirection.getY(), aTargetDirection.getX()) + M_PI_2); |
107 | | |
108 | | // rotate around docking position |
109 | 0 | aArrowTransform.rotate(fRotation); |
110 | | |
111 | | // move arrow docking position to polygon head |
112 | 0 | aArrowTransform.translate(aHead.getX(), aHead.getY()); |
113 | | |
114 | | // transform retval and close |
115 | 0 | aRetval.transform(aArrowTransform); |
116 | 0 | aRetval.setClosed(true); |
117 | | |
118 | | // if pConsumedLength is asked for, fill it |
119 | 0 | if(pConsumedLength) |
120 | 0 | { |
121 | 0 | *pConsumedLength = fConsumedLength; |
122 | 0 | } |
123 | 0 | } |
124 | |
|
125 | 0 | return aRetval; |
126 | 0 | } |
127 | | } // end of namespace |
128 | | |
129 | | namespace basegfx |
130 | | { |
131 | | // anonymous namespace for local helpers |
132 | | namespace |
133 | | { |
134 | | bool impIsSimpleEdge(const B2DCubicBezier& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad) |
135 | 229k | { |
136 | | // isBezier() is true, already tested by caller |
137 | 229k | const B2DVector aEdge(rCandidate.getEndPoint() - rCandidate.getStartPoint()); |
138 | | |
139 | 229k | if(aEdge.equalZero()) |
140 | 164 | { |
141 | | // start and end point the same, but control vectors used -> balloon curve loop |
142 | | // is not a simple edge |
143 | 164 | return false; |
144 | 164 | } |
145 | | |
146 | | // get tangentA and scalar with edge |
147 | 229k | const B2DVector aTangentA(rCandidate.getTangent(0.0)); |
148 | 229k | const double fScalarAE(aEdge.scalar(aTangentA)); |
149 | | |
150 | 229k | if(fScalarAE <= 0.0) |
151 | 1.07k | { |
152 | | // angle between TangentA and Edge is bigger or equal 90 degrees |
153 | 1.07k | return false; |
154 | 1.07k | } |
155 | | |
156 | | // get self-scalars for E and A |
157 | 227k | const double fScalarE(aEdge.scalar(aEdge)); |
158 | 227k | const double fScalarA(aTangentA.scalar(aTangentA)); |
159 | 227k | const double fLengthCompareE(fScalarE * fMaxPartOfEdgeQuad); |
160 | | |
161 | 227k | if(fTools::moreOrEqual(fScalarA, fLengthCompareE)) |
162 | 64.8k | { |
163 | | // length of TangentA is more than fMaxPartOfEdge of length of edge |
164 | 64.8k | return false; |
165 | 64.8k | } |
166 | | |
167 | 163k | if(fTools::lessOrEqual(fScalarAE * fScalarAE, fScalarA * fScalarE * fMaxCosQuad)) |
168 | 24.7k | { |
169 | | // angle between TangentA and Edge is bigger or equal angle defined by fMaxCos |
170 | 24.7k | return false; |
171 | 24.7k | } |
172 | | |
173 | | // get tangentB and scalar with edge |
174 | 138k | const B2DVector aTangentB(rCandidate.getTangent(1.0)); |
175 | 138k | const double fScalarBE(aEdge.scalar(aTangentB)); |
176 | | |
177 | 138k | if(fScalarBE <= 0.0) |
178 | 146 | { |
179 | | // angle between TangentB and Edge is bigger or equal 90 degrees |
180 | 146 | return false; |
181 | 146 | } |
182 | | |
183 | | // get self-scalar for B |
184 | 138k | const double fScalarB(aTangentB.scalar(aTangentB)); |
185 | | |
186 | 138k | if(fTools::moreOrEqual(fScalarB, fLengthCompareE)) |
187 | 37.3k | { |
188 | | // length of TangentB is more than fMaxPartOfEdge of length of edge |
189 | 37.3k | return false; |
190 | 37.3k | } |
191 | | |
192 | 100k | if(fTools::lessOrEqual(fScalarBE * fScalarBE, fScalarB * fScalarE * fMaxCosQuad)) |
193 | 1.08k | { |
194 | | // angle between TangentB and Edge is bigger or equal defined by fMaxCos |
195 | 1.08k | return false; |
196 | 1.08k | } |
197 | | |
198 | 99.7k | return true; |
199 | 100k | } |
200 | | |
201 | | void impSubdivideToSimple(const B2DCubicBezier& rCandidate, B2DPolygon& rTarget, double fMaxCosQuad, double fMaxPartOfEdgeQuad, sal_uInt32 nMaxRecursionDepth) |
202 | 271k | { |
203 | 271k | if(!nMaxRecursionDepth || impIsSimpleEdge(rCandidate, fMaxCosQuad, fMaxPartOfEdgeQuad)) |
204 | 142k | { |
205 | 142k | rTarget.appendBezierSegment(rCandidate.getControlPointA(), rCandidate.getControlPointB(), rCandidate.getEndPoint()); |
206 | 142k | } |
207 | 129k | else |
208 | 129k | { |
209 | 129k | B2DCubicBezier aLeft, aRight; |
210 | 129k | rCandidate.split(0.5, &aLeft, &aRight); |
211 | | |
212 | 129k | impSubdivideToSimple(aLeft, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1); |
213 | 129k | impSubdivideToSimple(aRight, rTarget, fMaxCosQuad, fMaxPartOfEdgeQuad, nMaxRecursionDepth - 1); |
214 | 129k | } |
215 | 271k | } |
216 | | |
217 | | B2DPolygon subdivideToSimple(const B2DPolygon& rCandidate, double fMaxCosQuad, double fMaxPartOfEdgeQuad) |
218 | 1.95k | { |
219 | 1.95k | const sal_uInt32 nPointCount(rCandidate.count()); |
220 | | |
221 | 1.95k | if(rCandidate.areControlPointsUsed() && nPointCount) |
222 | 51 | { |
223 | 51 | const sal_uInt32 nEdgeCount(rCandidate.isClosed() ? nPointCount : nPointCount - 1); |
224 | 51 | B2DPolygon aRetval; |
225 | 51 | B2DCubicBezier aEdge; |
226 | | |
227 | | // prepare edge for loop |
228 | 51 | aEdge.setStartPoint(rCandidate.getB2DPoint(0)); |
229 | 51 | aRetval.append(aEdge.getStartPoint()); |
230 | | |
231 | 19.2k | for(sal_uInt32 a(0); a < nEdgeCount; a++) |
232 | 19.2k | { |
233 | | // fill B2DCubicBezier |
234 | 19.2k | const sal_uInt32 nNextIndex((a + 1) % nPointCount); |
235 | 19.2k | aEdge.setControlPointA(rCandidate.getNextControlPoint(a)); |
236 | 19.2k | aEdge.setControlPointB(rCandidate.getPrevControlPoint(nNextIndex)); |
237 | 19.2k | aEdge.setEndPoint(rCandidate.getB2DPoint(nNextIndex)); |
238 | | |
239 | | // get rid of unnecessary bezier segments |
240 | 19.2k | aEdge.testAndSolveTrivialBezier(); |
241 | | |
242 | 19.2k | if(aEdge.isBezier()) |
243 | 7.84k | { |
244 | | // before splitting recursively with internal simple criteria, use |
245 | | // ExtremumPosFinder to remove those |
246 | 7.84k | std::vector< double > aExtremumPositions; |
247 | | |
248 | 7.84k | aExtremumPositions.reserve(4); |
249 | 7.84k | aEdge.getAllExtremumPositions(aExtremumPositions); |
250 | | |
251 | 7.84k | const sal_uInt32 nCount(aExtremumPositions.size()); |
252 | | |
253 | 7.84k | if(nCount) |
254 | 6.30k | { |
255 | 6.30k | if(nCount > 1) |
256 | 2.44k | { |
257 | | // create order from left to right |
258 | 2.44k | std::sort(aExtremumPositions.begin(), aExtremumPositions.end()); |
259 | 2.44k | } |
260 | | |
261 | 15.8k | for(sal_uInt32 b(0); b < nCount;) |
262 | 9.57k | { |
263 | | // split aEdge at next split pos |
264 | 9.57k | B2DCubicBezier aLeft; |
265 | 9.57k | const double fSplitPos(aExtremumPositions[b++]); |
266 | | |
267 | 9.57k | aEdge.split(fSplitPos, &aLeft, &aEdge); |
268 | 9.57k | aLeft.testAndSolveTrivialBezier(); |
269 | | |
270 | | // consume left part |
271 | 9.57k | if(aLeft.isBezier()) |
272 | 6.93k | { |
273 | 6.93k | impSubdivideToSimple(aLeft, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); |
274 | 6.93k | } |
275 | 2.63k | else |
276 | 2.63k | { |
277 | 2.63k | aRetval.append(aLeft.getEndPoint()); |
278 | 2.63k | } |
279 | | |
280 | 9.57k | if(b < nCount) |
281 | 3.27k | { |
282 | | // correct the remaining split positions to fit to shortened aEdge |
283 | 3.27k | const double fScaleFactor(1.0 / (1.0 - fSplitPos)); |
284 | | |
285 | 7.53k | for(sal_uInt32 c(b); c < nCount; c++) |
286 | 4.25k | { |
287 | 4.25k | aExtremumPositions[c] = (aExtremumPositions[c] - fSplitPos) * fScaleFactor; |
288 | 4.25k | } |
289 | 3.27k | } |
290 | 9.57k | } |
291 | | |
292 | | // test the shortened rest of aEdge |
293 | 6.30k | aEdge.testAndSolveTrivialBezier(); |
294 | | |
295 | | // consume right part |
296 | 6.30k | if(aEdge.isBezier()) |
297 | 4.16k | { |
298 | 4.16k | impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); |
299 | 4.16k | } |
300 | 2.13k | else |
301 | 2.13k | { |
302 | 2.13k | aRetval.append(aEdge.getEndPoint()); |
303 | 2.13k | } |
304 | 6.30k | } |
305 | 1.54k | else |
306 | 1.54k | { |
307 | 1.54k | impSubdivideToSimple(aEdge, aRetval, fMaxCosQuad, fMaxPartOfEdgeQuad, 6); |
308 | 1.54k | } |
309 | 7.84k | } |
310 | 11.3k | else |
311 | 11.3k | { |
312 | | // straight edge, add point |
313 | 11.3k | aRetval.append(aEdge.getEndPoint()); |
314 | 11.3k | } |
315 | | |
316 | | // prepare edge for next step |
317 | 19.2k | aEdge.setStartPoint(aEdge.getEndPoint()); |
318 | 19.2k | } |
319 | | |
320 | | // copy closed flag and check for double points |
321 | 51 | aRetval.setClosed(rCandidate.isClosed()); |
322 | 51 | aRetval.removeDoublePoints(); |
323 | | |
324 | 51 | return aRetval; |
325 | 51 | } |
326 | 1.90k | else |
327 | 1.90k | { |
328 | 1.90k | return rCandidate; |
329 | 1.90k | } |
330 | 1.95k | } |
331 | | |
332 | | B2DPolygon createAreaGeometryForEdge( |
333 | | const B2DCubicBezier& rEdge, |
334 | | double fHalfLineWidth, |
335 | | bool bStartRound, |
336 | | bool bEndRound, |
337 | | bool bStartSquare, |
338 | | bool bEndSquare) |
339 | 711k | { |
340 | | // create polygon for edge |
341 | | // Unfortunately, while it would be geometrically correct to not add |
342 | | // the in-between points EdgeEnd and EdgeStart, it leads to rounding |
343 | | // errors when converting to integer polygon coordinates for painting |
344 | 711k | if(rEdge.isBezier()) |
345 | 141k | { |
346 | | // prepare target and data common for upper and lower |
347 | 141k | B2DPolygon aBezierPolygon; |
348 | 141k | const B2DVector aPureEdgeVector(rEdge.getEndPoint() - rEdge.getStartPoint()); |
349 | 141k | const double fEdgeLength(aPureEdgeVector.getLength()); |
350 | 141k | const bool bIsEdgeLengthZero(fTools::equalZero(fEdgeLength)); |
351 | 141k | B2DVector aTangentA(rEdge.getTangent(0.0)); aTangentA.normalize(); |
352 | 141k | B2DVector aTangentB(rEdge.getTangent(1.0)); aTangentB.normalize(); |
353 | 141k | const B2DVector aNormalizedPerpendicularA(getPerpendicular(aTangentA)); |
354 | 141k | const B2DVector aNormalizedPerpendicularB(getPerpendicular(aTangentB)); |
355 | | |
356 | | // create upper displacement vectors and check if they cut |
357 | 141k | const B2DVector aPerpendStartA(aNormalizedPerpendicularA * -fHalfLineWidth); |
358 | 141k | const B2DVector aPerpendEndA(aNormalizedPerpendicularB * -fHalfLineWidth); |
359 | 141k | double fCutA(0.0); |
360 | 141k | const CutFlagValue aCutA(utils::findCut( |
361 | 141k | rEdge.getStartPoint(), aPerpendStartA, |
362 | 141k | rEdge.getEndPoint(), aPerpendEndA, |
363 | 141k | CutFlagValue::ALL, &fCutA)); |
364 | 141k | const bool bCutA(aCutA != CutFlagValue::NONE); |
365 | | |
366 | | // create lower displacement vectors and check if they cut |
367 | 141k | const B2DVector aPerpendStartB(aNormalizedPerpendicularA * fHalfLineWidth); |
368 | 141k | const B2DVector aPerpendEndB(aNormalizedPerpendicularB * fHalfLineWidth); |
369 | 141k | double fCutB(0.0); |
370 | 141k | const CutFlagValue aCutB(utils::findCut( |
371 | 141k | rEdge.getEndPoint(), aPerpendEndB, |
372 | 141k | rEdge.getStartPoint(), aPerpendStartB, |
373 | 141k | CutFlagValue::ALL, &fCutB)); |
374 | 141k | const bool bCutB(aCutB != CutFlagValue::NONE); |
375 | | |
376 | | // check if cut happens |
377 | 141k | const bool bCut(bCutA || bCutB); |
378 | 141k | B2DPoint aCutPoint; |
379 | | |
380 | | // create left edge |
381 | 141k | if(bStartRound || bStartSquare) |
382 | 0 | { |
383 | 0 | if(bStartRound) |
384 | 0 | { |
385 | 0 | basegfx::B2DPolygon aStartPolygon(utils::createHalfUnitCircle()); |
386 | |
|
387 | 0 | aStartPolygon.transform( |
388 | 0 | utils::createScaleShearXRotateTranslateB2DHomMatrix( |
389 | 0 | fHalfLineWidth, fHalfLineWidth, |
390 | 0 | 0.0, |
391 | 0 | atan2(aTangentA.getY(), aTangentA.getX()) + M_PI_2, |
392 | 0 | rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY())); |
393 | 0 | aBezierPolygon.append(aStartPolygon); |
394 | 0 | } |
395 | 0 | else // bStartSquare |
396 | 0 | { |
397 | 0 | const basegfx::B2DPoint aStart(rEdge.getStartPoint() - (aTangentA * fHalfLineWidth)); |
398 | |
|
399 | 0 | if(bCutB) |
400 | 0 | { |
401 | 0 | aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartB); |
402 | 0 | } |
403 | |
|
404 | 0 | aBezierPolygon.append(aStart + aPerpendStartB); |
405 | 0 | aBezierPolygon.append(aStart + aPerpendStartA); |
406 | |
|
407 | 0 | if(bCutA) |
408 | 0 | { |
409 | 0 | aBezierPolygon.append(rEdge.getStartPoint() + aPerpendStartA); |
410 | 0 | } |
411 | 0 | } |
412 | 0 | } |
413 | 141k | else |
414 | 141k | { |
415 | | // append original in-between point |
416 | 141k | aBezierPolygon.append(rEdge.getStartPoint()); |
417 | 141k | } |
418 | | |
419 | | // create upper edge. |
420 | 141k | { |
421 | 141k | if(bCutA) |
422 | 15.1k | { |
423 | | // calculate cut point and add |
424 | 15.1k | aCutPoint = rEdge.getStartPoint() + (aPerpendStartA * fCutA); |
425 | 15.1k | aBezierPolygon.append(aCutPoint); |
426 | 15.1k | } |
427 | 126k | else |
428 | 126k | { |
429 | | // create scaled bezier segment |
430 | 126k | const B2DPoint aStart(rEdge.getStartPoint() + aPerpendStartA); |
431 | 126k | const B2DPoint aEnd(rEdge.getEndPoint() + aPerpendEndA); |
432 | 126k | const B2DVector aEdge(aEnd - aStart); |
433 | 126k | const double fLength(aEdge.getLength()); |
434 | 126k | const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength); |
435 | 126k | const B2DVector fRelNext(rEdge.getControlPointA() - rEdge.getStartPoint()); |
436 | 126k | const B2DVector fRelPrev(rEdge.getControlPointB() - rEdge.getEndPoint()); |
437 | | |
438 | 126k | aBezierPolygon.append(aStart); |
439 | 126k | aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd); |
440 | 126k | } |
441 | 141k | } |
442 | | |
443 | | // create right edge |
444 | 141k | if(bEndRound || bEndSquare) |
445 | 0 | { |
446 | 0 | if(bEndRound) |
447 | 0 | { |
448 | 0 | basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle()); |
449 | |
|
450 | 0 | aEndPolygon.transform( |
451 | 0 | utils::createScaleShearXRotateTranslateB2DHomMatrix( |
452 | 0 | fHalfLineWidth, fHalfLineWidth, |
453 | 0 | 0.0, |
454 | 0 | atan2(aTangentB.getY(), aTangentB.getX()) - M_PI_2, |
455 | 0 | rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY())); |
456 | 0 | aBezierPolygon.append(aEndPolygon); |
457 | 0 | } |
458 | 0 | else // bEndSquare |
459 | 0 | { |
460 | 0 | const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + (aTangentB * fHalfLineWidth)); |
461 | |
|
462 | 0 | if(bCutA) |
463 | 0 | { |
464 | 0 | aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndA); |
465 | 0 | } |
466 | |
|
467 | 0 | aBezierPolygon.append(aEnd + aPerpendEndA); |
468 | 0 | aBezierPolygon.append(aEnd + aPerpendEndB); |
469 | |
|
470 | 0 | if(bCutB) |
471 | 0 | { |
472 | 0 | aBezierPolygon.append(rEdge.getEndPoint() + aPerpendEndB); |
473 | 0 | } |
474 | 0 | } |
475 | 0 | } |
476 | 141k | else |
477 | 141k | { |
478 | | // append original in-between point |
479 | 141k | aBezierPolygon.append(rEdge.getEndPoint()); |
480 | 141k | } |
481 | | |
482 | | // create lower edge. |
483 | 141k | { |
484 | 141k | if(bCutB) |
485 | 14.2k | { |
486 | | // calculate cut point and add |
487 | 14.2k | aCutPoint = rEdge.getEndPoint() + (aPerpendEndB * fCutB); |
488 | 14.2k | aBezierPolygon.append(aCutPoint); |
489 | 14.2k | } |
490 | 127k | else |
491 | 127k | { |
492 | | // create scaled bezier segment |
493 | 127k | const B2DPoint aStart(rEdge.getEndPoint() + aPerpendEndB); |
494 | 127k | const B2DPoint aEnd(rEdge.getStartPoint() + aPerpendStartB); |
495 | 127k | const B2DVector aEdge(aEnd - aStart); |
496 | 127k | const double fLength(aEdge.getLength()); |
497 | 127k | const double fScale(bIsEdgeLengthZero ? 1.0 : fLength / fEdgeLength); |
498 | 127k | const B2DVector fRelNext(rEdge.getControlPointB() - rEdge.getEndPoint()); |
499 | 127k | const B2DVector fRelPrev(rEdge.getControlPointA() - rEdge.getStartPoint()); |
500 | | |
501 | 127k | aBezierPolygon.append(aStart); |
502 | 127k | aBezierPolygon.appendBezierSegment(aStart + (fRelNext * fScale), aEnd + (fRelPrev * fScale), aEnd); |
503 | 127k | } |
504 | 141k | } |
505 | | |
506 | | // close |
507 | 141k | aBezierPolygon.setClosed(true); |
508 | | |
509 | 141k | if(bStartRound || bEndRound) |
510 | 0 | { |
511 | | // double points possible when round caps are used at start or end |
512 | 0 | aBezierPolygon.removeDoublePoints(); |
513 | 0 | } |
514 | | |
515 | 141k | if(bCut && ((bStartRound || bStartSquare) && (bEndRound || bEndSquare))) |
516 | 0 | { |
517 | | // When cut exists and both ends are extended with caps, a self-intersecting polygon |
518 | | // is created; one cut point is known, but there is a 2nd one in the caps geometry. |
519 | | // Solve by using tooling. |
520 | | // Remark: This nearly never happens due to curve preparations to extreme points |
521 | | // and maximum angle turning, but I constructed a test case and checked that it is |
522 | | // working properly. |
523 | 0 | const B2DPolyPolygon aTemp(utils::solveCrossovers(aBezierPolygon)); |
524 | 0 | const sal_uInt32 nTempCount(aTemp.count()); |
525 | |
|
526 | 0 | if(nTempCount) |
527 | 0 | { |
528 | 0 | if(nTempCount > 1) |
529 | 0 | { |
530 | | // as expected, multiple polygons (with same orientation). Remove |
531 | | // the one which contains aCutPoint, or better take the one without |
532 | 0 | for (sal_uInt32 a(0); a < nTempCount; a++) |
533 | 0 | { |
534 | 0 | aBezierPolygon = aTemp.getB2DPolygon(a); |
535 | |
|
536 | 0 | const sal_uInt32 nCandCount(aBezierPolygon.count()); |
537 | |
|
538 | 0 | for(sal_uInt32 b(0); b < nCandCount; b++) |
539 | 0 | { |
540 | 0 | if(aCutPoint.equal(aBezierPolygon.getB2DPoint(b))) |
541 | 0 | { |
542 | 0 | aBezierPolygon.clear(); |
543 | 0 | break; |
544 | 0 | } |
545 | 0 | } |
546 | |
|
547 | 0 | if(aBezierPolygon.count()) |
548 | 0 | { |
549 | 0 | break; |
550 | 0 | } |
551 | 0 | } |
552 | |
|
553 | 0 | OSL_ENSURE(aBezierPolygon.count(), "Error in line geometry creation, could not solve self-intersection (!)"); |
554 | 0 | } |
555 | 0 | else |
556 | 0 | { |
557 | | // none found, use result |
558 | 0 | aBezierPolygon = aTemp.getB2DPolygon(0); |
559 | 0 | } |
560 | 0 | } |
561 | 0 | else |
562 | 0 | { |
563 | 0 | OSL_ENSURE(false, "Error in line geometry creation, could not solve self-intersection (!)"); |
564 | 0 | } |
565 | 0 | } |
566 | | |
567 | | // return |
568 | 141k | return aBezierPolygon; |
569 | 141k | } |
570 | 569k | else |
571 | 569k | { |
572 | | // Get start and end point, create tangent and set to needed length |
573 | 569k | B2DVector aTangent(rEdge.getEndPoint() - rEdge.getStartPoint()); |
574 | 569k | aTangent.setLength(fHalfLineWidth); |
575 | | |
576 | | // prepare return value |
577 | 569k | B2DPolygon aEdgePolygon; |
578 | | |
579 | | // buffered angle |
580 | 569k | double fAngle(0.0); |
581 | 569k | bool bAngle(false); |
582 | | |
583 | | // buffered perpendicular |
584 | 569k | B2DVector aPerpend; |
585 | 569k | bool bPerpend(false); |
586 | | |
587 | | // create left vertical |
588 | 569k | if(bStartRound) |
589 | 0 | { |
590 | 0 | aEdgePolygon = utils::createHalfUnitCircle(); |
591 | 0 | fAngle = atan2(aTangent.getY(), aTangent.getX()); |
592 | 0 | bAngle = true; |
593 | 0 | aEdgePolygon.transform( |
594 | 0 | utils::createScaleShearXRotateTranslateB2DHomMatrix( |
595 | 0 | fHalfLineWidth, fHalfLineWidth, |
596 | 0 | 0.0, |
597 | 0 | fAngle + M_PI_2, |
598 | 0 | rEdge.getStartPoint().getX(), rEdge.getStartPoint().getY())); |
599 | 0 | } |
600 | 569k | else |
601 | 569k | { |
602 | 569k | aPerpend.setX(-aTangent.getY()); |
603 | 569k | aPerpend.setY(aTangent.getX()); |
604 | 569k | bPerpend = true; |
605 | | |
606 | 569k | if(bStartSquare) |
607 | 0 | { |
608 | 0 | const basegfx::B2DPoint aStart(rEdge.getStartPoint() - aTangent); |
609 | |
|
610 | 0 | aEdgePolygon.append(aStart + aPerpend); |
611 | 0 | aEdgePolygon.append(aStart - aPerpend); |
612 | 0 | } |
613 | 569k | else |
614 | 569k | { |
615 | 569k | aEdgePolygon.append(rEdge.getStartPoint() + aPerpend); |
616 | 569k | aEdgePolygon.append(rEdge.getStartPoint()); // keep the in-between point for numerical reasons |
617 | 569k | aEdgePolygon.append(rEdge.getStartPoint() - aPerpend); |
618 | 569k | } |
619 | 569k | } |
620 | | |
621 | | // create right vertical |
622 | 569k | if(bEndRound) |
623 | 0 | { |
624 | 0 | basegfx::B2DPolygon aEndPolygon(utils::createHalfUnitCircle()); |
625 | |
|
626 | 0 | if(!bAngle) |
627 | 0 | { |
628 | 0 | fAngle = atan2(aTangent.getY(), aTangent.getX()); |
629 | 0 | } |
630 | |
|
631 | 0 | aEndPolygon.transform( |
632 | 0 | utils::createScaleShearXRotateTranslateB2DHomMatrix( |
633 | 0 | fHalfLineWidth, fHalfLineWidth, |
634 | 0 | 0.0, |
635 | 0 | fAngle - M_PI_2, |
636 | 0 | rEdge.getEndPoint().getX(), rEdge.getEndPoint().getY())); |
637 | 0 | aEdgePolygon.append(aEndPolygon); |
638 | 0 | } |
639 | 569k | else |
640 | 569k | { |
641 | 569k | if(!bPerpend) |
642 | 0 | { |
643 | 0 | aPerpend.setX(-aTangent.getY()); |
644 | 0 | aPerpend.setY(aTangent.getX()); |
645 | 0 | } |
646 | | |
647 | 569k | if(bEndSquare) |
648 | 0 | { |
649 | 0 | const basegfx::B2DPoint aEnd(rEdge.getEndPoint() + aTangent); |
650 | |
|
651 | 0 | aEdgePolygon.append(aEnd - aPerpend); |
652 | 0 | aEdgePolygon.append(aEnd + aPerpend); |
653 | 0 | } |
654 | 569k | else |
655 | 569k | { |
656 | 569k | aEdgePolygon.append(rEdge.getEndPoint() - aPerpend); |
657 | 569k | aEdgePolygon.append(rEdge.getEndPoint()); // keep the in-between point for numerical reasons |
658 | 569k | aEdgePolygon.append(rEdge.getEndPoint() + aPerpend); |
659 | 569k | } |
660 | 569k | } |
661 | | |
662 | | // close and return |
663 | 569k | aEdgePolygon.setClosed(true); |
664 | | |
665 | 569k | return aEdgePolygon; |
666 | 569k | } |
667 | 711k | } |
668 | | |
669 | | B2DPolygon createAreaGeometryForJoin( |
670 | | const B2DVector& rTangentPrev, |
671 | | const B2DVector& rTangentEdge, |
672 | | const B2DVector& rPerpendPrev, |
673 | | const B2DVector& rPerpendEdge, |
674 | | const B2DPoint& rPoint, |
675 | | double fHalfLineWidth, |
676 | | B2DLineJoin eJoin, |
677 | | double fMiterMinimumAngle) |
678 | 204k | { |
679 | 204k | SAL_WARN_IF(fHalfLineWidth <= 0.0,"basegfx","createAreaGeometryForJoin: LineWidth too small (!)"); |
680 | 204k | assert((eJoin != B2DLineJoin::NONE) && "createAreaGeometryForJoin: B2DLineJoin::NONE not allowed (!)"); |
681 | | |
682 | | // LineJoin from tangent rPerpendPrev to tangent rPerpendEdge in rPoint |
683 | 204k | B2DPolygon aEdgePolygon; |
684 | 204k | const B2DPoint aStartPoint(rPoint + rPerpendPrev); |
685 | 204k | const B2DPoint aEndPoint(rPoint + rPerpendEdge); |
686 | | |
687 | | // test if for Miter, the angle is too small and the fallback |
688 | | // to bevel needs to be used |
689 | 204k | if(eJoin == B2DLineJoin::Miter) |
690 | 655 | { |
691 | 655 | const double fAngle(fabs(rPerpendPrev.angle(rPerpendEdge))); |
692 | | |
693 | 655 | if((M_PI - fAngle) < fMiterMinimumAngle) |
694 | 261 | { |
695 | | // fallback to bevel |
696 | 261 | eJoin = B2DLineJoin::Bevel; |
697 | 261 | } |
698 | 655 | } |
699 | | |
700 | 204k | switch(eJoin) |
701 | 204k | { |
702 | 394 | case B2DLineJoin::Miter : |
703 | 394 | { |
704 | 394 | aEdgePolygon.append(aEndPoint); |
705 | 394 | aEdgePolygon.append(rPoint); |
706 | 394 | aEdgePolygon.append(aStartPoint); |
707 | | |
708 | | // Look for the cut point between start point along rTangentPrev and |
709 | | // end point along rTangentEdge. -rTangentEdge should be used, but since |
710 | | // the cut value is used for interpolating along the first edge, the negation |
711 | | // is not needed since the same fCut will be found on the first edge. |
712 | | // If it exists, insert it to complete the mitered fill polygon. |
713 | 394 | double fCutPos(0.0); |
714 | 394 | utils::findCut(aStartPoint, rTangentPrev, aEndPoint, rTangentEdge, CutFlagValue::ALL, &fCutPos); |
715 | | |
716 | 394 | if(fCutPos != 0.0) |
717 | 393 | { |
718 | 393 | const B2DPoint aCutPoint(aStartPoint + (rTangentPrev * fCutPos)); |
719 | 393 | aEdgePolygon.append(aCutPoint); |
720 | 393 | } |
721 | | |
722 | 394 | break; |
723 | 0 | } |
724 | 181k | case B2DLineJoin::Round : |
725 | 181k | { |
726 | | // use tooling to add needed EllipseSegment |
727 | 181k | double fAngleStart(atan2(rPerpendPrev.getY(), rPerpendPrev.getX())); |
728 | 181k | double fAngleEnd(atan2(rPerpendEdge.getY(), rPerpendEdge.getX())); |
729 | | |
730 | | // atan2 results are [-PI .. PI], consolidate to [0.0 .. 2PI] |
731 | 181k | if(fAngleStart < 0.0) |
732 | 91.3k | { |
733 | 91.3k | fAngleStart += 2 * M_PI; |
734 | 91.3k | } |
735 | | |
736 | 181k | if(fAngleEnd < 0.0) |
737 | 91.6k | { |
738 | 91.6k | fAngleEnd += 2 * M_PI; |
739 | 91.6k | } |
740 | | |
741 | 181k | const B2DPolygon aBow(utils::createPolygonFromEllipseSegment(rPoint, fHalfLineWidth, fHalfLineWidth, fAngleStart, fAngleEnd)); |
742 | | |
743 | 181k | if(aBow.count() > 1) |
744 | 181k | { |
745 | | // #i101491# |
746 | | // use the original start/end positions; the ones from bow creation may be numerically |
747 | | // different due to their different creation. To guarantee good merging quality with edges |
748 | | // and edge roundings (and to reduce point count) |
749 | 181k | aEdgePolygon = aBow; |
750 | 181k | aEdgePolygon.setB2DPoint(0, aStartPoint); |
751 | 181k | aEdgePolygon.setB2DPoint(aEdgePolygon.count() - 1, aEndPoint); |
752 | 181k | aEdgePolygon.append(rPoint); |
753 | | |
754 | 181k | break; |
755 | 181k | } |
756 | 0 | else |
757 | 0 | { |
758 | 0 | [[fallthrough]]; // wanted fall-through to default |
759 | 0 | } |
760 | 181k | } |
761 | 23.1k | default: // B2DLineJoin::Bevel |
762 | 23.1k | { |
763 | 23.1k | aEdgePolygon.append(aEndPoint); |
764 | 23.1k | aEdgePolygon.append(rPoint); |
765 | 23.1k | aEdgePolygon.append(aStartPoint); |
766 | | |
767 | 23.1k | break; |
768 | 181k | } |
769 | 204k | } |
770 | | |
771 | | // create last polygon part for edge |
772 | 204k | aEdgePolygon.setClosed(true); |
773 | | |
774 | 204k | return aEdgePolygon; |
775 | 204k | } |
776 | | } // end of anonymous namespace |
777 | | |
778 | | namespace utils |
779 | | { |
780 | | B2DPolyPolygon createAreaGeometry( |
781 | | const B2DPolygon& rCandidate, |
782 | | double fHalfLineWidth, |
783 | | B2DLineJoin eJoin, |
784 | | css::drawing::LineCap eCap, |
785 | | double fMaxAllowedAngle, |
786 | | double fMaxPartOfEdge, |
787 | | double fMiterMinimumAngle) |
788 | 1.95k | { |
789 | 1.95k | if(fMaxAllowedAngle > M_PI_2) |
790 | 0 | { |
791 | 0 | fMaxAllowedAngle = M_PI_2; |
792 | 0 | } |
793 | 1.95k | else if(fMaxAllowedAngle < 0.01 * M_PI_2) |
794 | 0 | { |
795 | 0 | fMaxAllowedAngle = 0.01 * M_PI_2; |
796 | 0 | } |
797 | | |
798 | 1.95k | if(fMaxPartOfEdge > 1.0) |
799 | 0 | { |
800 | 0 | fMaxPartOfEdge = 1.0; |
801 | 0 | } |
802 | 1.95k | else if(fMaxPartOfEdge < 0.01) |
803 | 0 | { |
804 | 0 | fMaxPartOfEdge = 0.01; |
805 | 0 | } |
806 | | |
807 | 1.95k | if(fMiterMinimumAngle > M_PI) |
808 | 0 | { |
809 | 0 | fMiterMinimumAngle = M_PI; |
810 | 0 | } |
811 | 1.95k | else if(fMiterMinimumAngle < 0.01 * M_PI) |
812 | 0 | { |
813 | 0 | fMiterMinimumAngle = 0.01 * M_PI; |
814 | 0 | } |
815 | | |
816 | 1.95k | B2DPolygon aCandidate(rCandidate); |
817 | 1.95k | const double fMaxCos(cos(fMaxAllowedAngle)); |
818 | | |
819 | 1.95k | aCandidate.removeDoublePoints(); |
820 | 1.95k | aCandidate = subdivideToSimple(aCandidate, fMaxCos * fMaxCos, fMaxPartOfEdge * fMaxPartOfEdge); |
821 | | |
822 | 1.95k | const sal_uInt32 nPointCount(aCandidate.count()); |
823 | | |
824 | 1.95k | if(nPointCount) |
825 | 1.95k | { |
826 | 1.95k | B2DPolyPolygon aRetval; |
827 | 1.95k | const bool bIsClosed(aCandidate.isClosed()); |
828 | 1.95k | const sal_uInt32 nEdgeCount(bIsClosed ? nPointCount : nPointCount - 1); |
829 | 1.95k | const bool bLineCap(!bIsClosed && eCap != css::drawing::LineCap_BUTT); |
830 | | |
831 | 1.95k | if(nEdgeCount) |
832 | 1.94k | { |
833 | 1.94k | B2DCubicBezier aEdge; |
834 | 1.94k | B2DCubicBezier aPrev; |
835 | | |
836 | 1.94k | const bool bEventuallyCreateLineJoin(eJoin != B2DLineJoin::NONE); |
837 | | // prepare edge |
838 | 1.94k | aEdge.setStartPoint(aCandidate.getB2DPoint(0)); |
839 | | |
840 | 1.94k | if(bIsClosed && bEventuallyCreateLineJoin) |
841 | 5 | { |
842 | | // prepare previous edge |
843 | 5 | const sal_uInt32 nPrevIndex(nPointCount - 1); |
844 | 5 | aPrev.setStartPoint(aCandidate.getB2DPoint(nPrevIndex)); |
845 | 5 | aPrev.setControlPointA(aCandidate.getNextControlPoint(nPrevIndex)); |
846 | 5 | aPrev.setControlPointB(aCandidate.getPrevControlPoint(0)); |
847 | 5 | aPrev.setEndPoint(aEdge.getStartPoint()); |
848 | 5 | } |
849 | | |
850 | 713k | for(sal_uInt32 a(0); a < nEdgeCount; a++) |
851 | 711k | { |
852 | | // fill current Edge |
853 | 711k | const sal_uInt32 nNextIndex((a + 1) % nPointCount); |
854 | 711k | aEdge.setControlPointA(aCandidate.getNextControlPoint(a)); |
855 | 711k | aEdge.setControlPointB(aCandidate.getPrevControlPoint(nNextIndex)); |
856 | 711k | aEdge.setEndPoint(aCandidate.getB2DPoint(nNextIndex)); |
857 | | |
858 | | // check and create linejoin |
859 | 711k | if(bEventuallyCreateLineJoin && (bIsClosed || a != 0)) |
860 | 667k | { |
861 | 667k | B2DVector aTangentPrev(aPrev.getTangent(1.0)); aTangentPrev.normalize(); |
862 | 667k | B2DVector aTangentEdge(aEdge.getTangent(0.0)); aTangentEdge.normalize(); |
863 | 667k | B2VectorOrientation aOrientation(getOrientation(aTangentPrev, aTangentEdge)); |
864 | | |
865 | 667k | if(aOrientation == B2VectorOrientation::Neutral) |
866 | 481k | { |
867 | | // they are parallel or empty; if they are both not zero and point |
868 | | // in opposite direction, a half-circle is needed |
869 | 481k | if(!aTangentPrev.equalZero() && !aTangentEdge.equalZero()) |
870 | 481k | { |
871 | 481k | const double fAngle(fabs(aTangentPrev.angle(aTangentEdge))); |
872 | | |
873 | 481k | if(fTools::equal(fAngle, M_PI)) |
874 | 19.8k | { |
875 | | // for half-circle production, fallback to positive |
876 | | // orientation |
877 | 19.8k | aOrientation = B2VectorOrientation::Positive; |
878 | 19.8k | } |
879 | 481k | } |
880 | 481k | } |
881 | | |
882 | 667k | if(aOrientation == B2VectorOrientation::Positive) |
883 | 110k | { |
884 | 110k | const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * -fHalfLineWidth); |
885 | 110k | const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * -fHalfLineWidth); |
886 | | |
887 | 110k | aRetval.append( |
888 | 110k | createAreaGeometryForJoin( |
889 | 110k | aTangentPrev, |
890 | 110k | aTangentEdge, |
891 | 110k | aPerpendPrev, |
892 | 110k | aPerpendEdge, |
893 | 110k | aEdge.getStartPoint(), |
894 | 110k | fHalfLineWidth, |
895 | 110k | eJoin, |
896 | 110k | fMiterMinimumAngle)); |
897 | 110k | } |
898 | 556k | else if(aOrientation == B2VectorOrientation::Negative) |
899 | 94.5k | { |
900 | 94.5k | const B2DVector aPerpendPrev(getPerpendicular(aTangentPrev) * fHalfLineWidth); |
901 | 94.5k | const B2DVector aPerpendEdge(getPerpendicular(aTangentEdge) * fHalfLineWidth); |
902 | | |
903 | 94.5k | aRetval.append( |
904 | 94.5k | createAreaGeometryForJoin( |
905 | 94.5k | aTangentEdge, |
906 | 94.5k | aTangentPrev, |
907 | 94.5k | aPerpendEdge, |
908 | 94.5k | aPerpendPrev, |
909 | 94.5k | aEdge.getStartPoint(), |
910 | 94.5k | fHalfLineWidth, |
911 | 94.5k | eJoin, |
912 | 94.5k | fMiterMinimumAngle)); |
913 | 94.5k | } |
914 | 667k | } |
915 | | |
916 | | // create geometry for edge |
917 | 711k | const bool bLast(a + 1 == nEdgeCount); |
918 | | |
919 | 711k | if(bLineCap) |
920 | 114k | { |
921 | 114k | const bool bFirst(!a); |
922 | | |
923 | 114k | aRetval.append( |
924 | 114k | createAreaGeometryForEdge( |
925 | 114k | aEdge, |
926 | 114k | fHalfLineWidth, |
927 | 114k | bFirst && eCap == css::drawing::LineCap_ROUND, |
928 | 114k | bLast && eCap == css::drawing::LineCap_ROUND, |
929 | 114k | bFirst && eCap == css::drawing::LineCap_SQUARE, |
930 | 114k | bLast && eCap == css::drawing::LineCap_SQUARE)); |
931 | 114k | } |
932 | 596k | else |
933 | 596k | { |
934 | 596k | aRetval.append( |
935 | 596k | createAreaGeometryForEdge( |
936 | 596k | aEdge, |
937 | 596k | fHalfLineWidth, |
938 | 596k | false, |
939 | 596k | false, |
940 | 596k | false, |
941 | 596k | false)); |
942 | 596k | } |
943 | | |
944 | | // prepare next step |
945 | 711k | if(!bLast) |
946 | 709k | { |
947 | 709k | if(bEventuallyCreateLineJoin) |
948 | 667k | { |
949 | 667k | aPrev = aEdge; |
950 | 667k | } |
951 | | |
952 | 709k | aEdge.setStartPoint(aEdge.getEndPoint()); |
953 | 709k | } |
954 | 711k | } |
955 | 1.94k | } |
956 | 12 | else |
957 | 12 | { |
958 | | // point count, but no edge count -> single point |
959 | 12 | const basegfx::B2DPolygon aCircle( |
960 | 12 | createPolygonFromCircle( |
961 | 12 | aCandidate.getB2DPoint(0), |
962 | 12 | fHalfLineWidth)); |
963 | | |
964 | 12 | aRetval.append(aCircle); |
965 | 12 | } |
966 | | |
967 | 1.95k | return aRetval; |
968 | 1.95k | } |
969 | 0 | else |
970 | 0 | { |
971 | 0 | return B2DPolyPolygon(rCandidate); |
972 | 0 | } |
973 | 1.95k | } |
974 | | } // end of namespace utils |
975 | | } // end of namespace basegfx |
976 | | |
977 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |