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