1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <osl/diagnose.h>
21 #include <basegfx/polygon/b3dpolygontools.hxx>
22 #include <basegfx/polygon/b3dpolygon.hxx>
23 #include <basegfx/polygon/b3dpolypolygon.hxx>
24 #include <basegfx/numeric/ftools.hxx>
25 #include <basegfx/range/b3drange.hxx>
26 #include <basegfx/point/b2dpoint.hxx>
27 #include <basegfx/tuple/b3ituple.hxx>
31 namespace basegfx::utils
34 void checkClosed(B3DPolygon
& rCandidate
)
36 while(rCandidate
.count() > 1
37 && rCandidate
.getB3DPoint(0).equal(rCandidate
.getB3DPoint(rCandidate
.count() - 1)))
39 rCandidate
.setClosed(true);
40 rCandidate
.remove(rCandidate
.count() - 1);
44 sal_uInt32
getIndexOfSuccessor(sal_uInt32 nIndex
, const B3DPolygon
& rCandidate
)
46 OSL_ENSURE(nIndex
< rCandidate
.count(), "getIndexOfPredecessor: Access to polygon out of range (!)");
48 if(nIndex
+ 1 < rCandidate
.count())
58 B3DRange
getRange(const B3DPolygon
& rCandidate
)
61 const sal_uInt32
nPointCount(rCandidate
.count());
63 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
65 const B3DPoint
aTestPoint(rCandidate
.getB3DPoint(a
));
66 aRetval
.expand(aTestPoint
);
72 double getLength(const B3DPolygon
& rCandidate
)
75 const sal_uInt32
nPointCount(rCandidate
.count());
79 const sal_uInt32
nLoopCount(rCandidate
.isClosed() ? nPointCount
: nPointCount
- 1);
81 for(sal_uInt32
a(0); a
< nLoopCount
; a
++)
83 const sal_uInt32
nNextIndex(getIndexOfSuccessor(a
, rCandidate
));
84 const B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(a
));
85 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
86 const B3DVector
aVector(aNextPoint
- aCurrentPoint
);
87 fRetval
+= aVector
.getLength();
94 void applyLineDashing(
95 const B3DPolygon
& rCandidate
,
96 const std::vector
<double>& rDotDashArray
,
97 B3DPolyPolygon
* pLineTarget
,
98 double fDotDashLength
)
100 // clear targets in any case
103 pLineTarget
->clear();
106 // call version that uses callbacks
110 // provide callback as lambda
112 ? std::function
<void(const basegfx::B3DPolygon
&)>()
113 : [&pLineTarget
](const basegfx::B3DPolygon
& rSnippet
){ pLineTarget
->append(rSnippet
); }),
117 static void implHandleSnippet(
118 const B3DPolygon
& rSnippet
,
119 const std::function
<void(const basegfx::B3DPolygon
& rSnippet
)>& rTargetCallback
,
123 if(rSnippet
.isClosed())
133 rTargetCallback(rLast
);
141 rTargetCallback(rSnippet
);
145 static void implHandleFirstLast(
146 const std::function
<void(const basegfx::B3DPolygon
& rSnippet
)>& rTargetCallback
,
150 if(rFirst
.count() && rLast
.count()
151 && rFirst
.getB3DPoint(0).equal(rLast
.getB3DPoint(rLast
.count() - 1)))
153 // start of first and end of last are the same -> merge them
154 rLast
.append(rFirst
);
155 rLast
.removeDoublePoints();
161 rTargetCallback(rLast
);
166 rTargetCallback(rFirst
);
170 void applyLineDashing(
171 const B3DPolygon
& rCandidate
,
172 const std::vector
<double>& rDotDashArray
,
173 const std::function
<void(const basegfx::B3DPolygon
& rSnippet
)>& rLineTargetCallback
,
174 double fDotDashLength
)
176 const sal_uInt32
nPointCount(rCandidate
.count());
177 const sal_uInt32
nDotDashCount(rDotDashArray
.size());
179 if(fDotDashLength
<= 0.0)
181 fDotDashLength
= std::accumulate(rDotDashArray
.begin(), rDotDashArray
.end(), 0.0);
184 if(fDotDashLength
<= 0.0 || !rLineTargetCallback
|| !nPointCount
)
186 // parameters make no sense, just add source to targets
187 if (rLineTargetCallback
)
188 rLineTargetCallback(rCandidate
);
192 // precalculate maximal acceptable length of candidate polygon assuming
193 // we want to create a maximum of fNumberOfAllowedSnippets. In 3D
194 // use less for fNumberOfAllowedSnippets, ca. 6553.6, double due to line & gap.
195 // Less in 3D due to potentially blowing up to rounded line segments.
196 static const double fNumberOfAllowedSnippets(6553.5 * 2.0);
197 const double fAllowedLength((fNumberOfAllowedSnippets
* fDotDashLength
) / double(rDotDashArray
.size()));
198 const double fCandidateLength(basegfx::utils::getLength(rCandidate
));
199 std::vector
<double> aDotDashArray(rDotDashArray
);
201 if(fCandidateLength
> fAllowedLength
)
203 // we would produce more than fNumberOfAllowedSnippets, so
204 // adapt aDotDashArray to exactly produce assumed number. Also
205 // assert this to let the caller know about it.
206 // If this asserts: Please think about checking your DotDashArray
207 // before calling this function or evtl. use the callback version
208 // to *not* produce that much of data. Even then, you may still
209 // think about producing too much runtime (!)
210 assert(true && "applyLineDashing: potentially too expensive to do the requested dismantle - please consider stretched LineDash pattern (!)");
212 // calculate correcting factor, apply to aDotDashArray and fDotDashLength
213 // to enlarge these as needed
214 const double fFactor(fCandidateLength
/ fAllowedLength
);
215 std::for_each(aDotDashArray
.begin(), aDotDashArray
.end(), [&fFactor
](double &f
){ f
*= fFactor
; });
218 // prepare current edge's start
219 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(0));
220 const bool bIsClosed(rCandidate
.isClosed());
221 const sal_uInt32
nEdgeCount(bIsClosed
? nPointCount
: nPointCount
- 1);
223 // prepare DotDashArray iteration and the line/gap switching bool
224 sal_uInt32
nDotDashIndex(0);
226 double fDotDashMovingLength(aDotDashArray
[0]);
229 // remember 1st and last snippets to try to merge after execution
230 // is complete and hand to callback
231 B3DPolygon aFirstLine
, aLastLine
;
233 // iterate over all edges
234 for(sal_uInt32
a(0); a
< nEdgeCount
; a
++)
236 // update current edge
237 const sal_uInt32
nNextIndex((a
+ 1) % nPointCount
);
238 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
239 const double fEdgeLength(B3DVector(aNextPoint
- aCurrentPoint
).getLength());
241 if(!fTools::equalZero(fEdgeLength
))
243 double fLastDotDashMovingLength(0.0);
244 while(fTools::less(fDotDashMovingLength
, fEdgeLength
))
246 // new split is inside edge, create and append snippet [fLastDotDashMovingLength, fDotDashMovingLength]
249 if(!aSnippet
.count())
251 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fLastDotDashMovingLength
/ fEdgeLength
));
254 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fDotDashMovingLength
/ fEdgeLength
));
256 implHandleSnippet(aSnippet
, rLineTargetCallback
, aFirstLine
, aLastLine
);
261 // prepare next DotDashArray step and flip line/gap flag
262 fLastDotDashMovingLength
= fDotDashMovingLength
;
263 fDotDashMovingLength
+= aDotDashArray
[(++nDotDashIndex
) % nDotDashCount
];
267 // append snippet [fLastDotDashMovingLength, fEdgeLength]
270 if(!aSnippet
.count())
272 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fLastDotDashMovingLength
/ fEdgeLength
));
275 aSnippet
.append(aNextPoint
);
278 // prepare move to next edge
279 fDotDashMovingLength
-= fEdgeLength
;
282 // prepare next edge step (end point gets new start point)
283 aCurrentPoint
= aNextPoint
;
286 // append last intermediate results (if exists)
291 implHandleSnippet(aSnippet
, rLineTargetCallback
, aFirstLine
, aLastLine
);
297 implHandleFirstLast(rLineTargetCallback
, aFirstLine
, aLastLine
);
301 B3DPolygon
applyDefaultNormalsSphere( const B3DPolygon
& rCandidate
, const B3DPoint
& rCenter
)
303 B3DPolygon
aRetval(rCandidate
);
305 for(sal_uInt32
a(0); a
< aRetval
.count(); a
++)
307 B3DVector
aVector(aRetval
.getB3DPoint(a
) - rCenter
);
309 aRetval
.setNormal(a
, aVector
);
315 B3DPolygon
invertNormals( const B3DPolygon
& rCandidate
)
317 B3DPolygon
aRetval(rCandidate
);
319 if(aRetval
.areNormalsUsed())
321 for(sal_uInt32
a(0); a
< aRetval
.count(); a
++)
323 aRetval
.setNormal(a
, -aRetval
.getNormal(a
));
330 B3DPolygon
applyDefaultTextureCoordinatesParallel( const B3DPolygon
& rCandidate
, const B3DRange
& rRange
, bool bChangeX
, bool bChangeY
)
332 B3DPolygon
aRetval(rCandidate
);
334 if(bChangeX
|| bChangeY
)
336 // create projection of standard texture coordinates in (X, Y) onto
337 // the 3d coordinates straight
338 const double fWidth(rRange
.getWidth());
339 const double fHeight(rRange
.getHeight());
340 const bool bWidthSet(!fTools::equalZero(fWidth
));
341 const bool bHeightSet(!fTools::equalZero(fHeight
));
342 const double fOne(1.0);
344 for(sal_uInt32
a(0); a
< aRetval
.count(); a
++)
346 const B3DPoint
aPoint(aRetval
.getB3DPoint(a
));
347 B2DPoint
aTextureCoordinate(aRetval
.getTextureCoordinate(a
));
353 aTextureCoordinate
.setX((aPoint
.getX() - rRange
.getMinX()) / fWidth
);
357 aTextureCoordinate
.setX(0.0);
365 aTextureCoordinate
.setY(fOne
- ((aPoint
.getY() - rRange
.getMinY()) / fHeight
));
369 aTextureCoordinate
.setY(fOne
);
373 aRetval
.setTextureCoordinate(a
, aTextureCoordinate
);
380 B3DPolygon
applyDefaultTextureCoordinatesSphere( const B3DPolygon
& rCandidate
, const B3DPoint
& rCenter
, bool bChangeX
, bool bChangeY
)
382 B3DPolygon
aRetval(rCandidate
);
384 if(bChangeX
|| bChangeY
)
386 // create texture coordinates using sphere projection to cartesian coordinates,
387 // use object's center as base
388 const double fOne(1.0);
389 const sal_uInt32
nPointCount(aRetval
.count());
390 bool bPolarPoints(false);
393 // create center cartesian coordinates to have a possibility to decide if on boundary
394 // transitions which value to choose
395 const B3DRange
aPlaneRange(getRange(rCandidate
));
396 const B3DPoint
aPlaneCenter(aPlaneRange
.getCenter() - rCenter
);
397 const double fXCenter(fOne
- ((atan2(aPlaneCenter
.getZ(), aPlaneCenter
.getX()) + M_PI
) / (2 * M_PI
)));
399 for(a
= 0; a
< nPointCount
; a
++)
401 const B3DVector
aVector(aRetval
.getB3DPoint(a
) - rCenter
);
402 const double fY(fOne
- ((atan2(aVector
.getY(), aVector
.getXZLength()) + M_PI_2
) / M_PI
));
403 B2DPoint
aTexCoor(aRetval
.getTextureCoordinate(a
));
405 if(fTools::equalZero(fY
))
407 // point is a north polar point, no useful X-coordinate can be created.
418 else if(fTools::equal(fY
, fOne
))
420 // point is a south polar point, no useful X-coordinate can be created. Set
421 // Y-coordinate, though
434 double fX(fOne
- ((atan2(aVector
.getZ(), aVector
.getX()) + M_PI
) / (2 * M_PI
)));
436 // correct cartesian point coordinate dependent from center value
437 if(fX
> fXCenter
+ 0.5)
441 else if(fX
< fXCenter
- 0.5)
457 aRetval
.setTextureCoordinate(a
, aTexCoor
);
462 // correct X-texture coordinates if polar points are contained. Those
463 // coordinates cannot be correct, so use prev or next X-coordinate
464 for(a
= 0; a
< nPointCount
; a
++)
466 B2DPoint
aTexCoor(aRetval
.getTextureCoordinate(a
));
468 if(fTools::equalZero(aTexCoor
.getY()) || fTools::equal(aTexCoor
.getY(), fOne
))
470 // get prev, next TexCoor and test for pole
471 const B2DPoint
aPrevTexCoor(aRetval
.getTextureCoordinate(a
? a
- 1 : nPointCount
- 1));
472 const B2DPoint
aNextTexCoor(aRetval
.getTextureCoordinate((a
+ 1) % nPointCount
));
473 const bool bPrevPole(fTools::equalZero(aPrevTexCoor
.getY()) || fTools::equal(aPrevTexCoor
.getY(), fOne
));
474 const bool bNextPole(fTools::equalZero(aNextTexCoor
.getY()) || fTools::equal(aNextTexCoor
.getY(), fOne
));
476 if(!bPrevPole
&& !bNextPole
)
478 // both no poles, mix them
479 aTexCoor
.setX((aPrevTexCoor
.getX() + aNextTexCoor
.getX()) / 2.0);
484 aTexCoor
.setX(aNextTexCoor
.getX());
488 // copy prev, even if it's a pole, hopefully it is already corrected
489 aTexCoor
.setX(aPrevTexCoor
.getX());
492 aRetval
.setTextureCoordinate(a
, aTexCoor
);
501 bool isInside(const B3DPolygon
& rCandidate
, const B3DPoint
& rPoint
, bool bWithBorder
)
503 if(bWithBorder
&& isPointOnPolygon(rCandidate
, rPoint
))
510 const B3DVector
aPlaneNormal(rCandidate
.getNormal());
512 if(!aPlaneNormal
.equalZero())
514 const sal_uInt32
nPointCount(rCandidate
.count());
518 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(nPointCount
- 1));
519 const double fAbsX(fabs(aPlaneNormal
.getX()));
520 const double fAbsY(fabs(aPlaneNormal
.getY()));
521 const double fAbsZ(fabs(aPlaneNormal
.getZ()));
523 if(fAbsX
> fAbsY
&& fAbsX
> fAbsZ
)
525 // normal points mostly in X-Direction, use YZ-Polygon projection for check
527 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
529 const B3DPoint
aPreviousPoint(aCurrentPoint
);
530 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
533 const bool bCompZA(fTools::more(aPreviousPoint
.getZ(), rPoint
.getZ()));
534 const bool bCompZB(fTools::more(aCurrentPoint
.getZ(), rPoint
.getZ()));
536 if(bCompZA
!= bCompZB
)
539 const bool bCompYA(fTools::more(aPreviousPoint
.getY(), rPoint
.getY()));
540 const bool bCompYB(fTools::more(aCurrentPoint
.getY(), rPoint
.getY()));
542 if(bCompYA
== bCompYB
)
551 const double fCompare(
552 aCurrentPoint
.getY() - (aCurrentPoint
.getZ() - rPoint
.getZ()) *
553 (aPreviousPoint
.getY() - aCurrentPoint
.getY()) /
554 (aPreviousPoint
.getZ() - aCurrentPoint
.getZ()));
556 if(fTools::more(fCompare
, rPoint
.getY()))
564 else if(fAbsY
> fAbsX
&& fAbsY
> fAbsZ
)
566 // normal points mostly in Y-Direction, use XZ-Polygon projection for check
568 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
570 const B3DPoint
aPreviousPoint(aCurrentPoint
);
571 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
574 const bool bCompZA(fTools::more(aPreviousPoint
.getZ(), rPoint
.getZ()));
575 const bool bCompZB(fTools::more(aCurrentPoint
.getZ(), rPoint
.getZ()));
577 if(bCompZA
!= bCompZB
)
580 const bool bCompXA(fTools::more(aPreviousPoint
.getX(), rPoint
.getX()));
581 const bool bCompXB(fTools::more(aCurrentPoint
.getX(), rPoint
.getX()));
583 if(bCompXA
== bCompXB
)
592 const double fCompare(
593 aCurrentPoint
.getX() - (aCurrentPoint
.getZ() - rPoint
.getZ()) *
594 (aPreviousPoint
.getX() - aCurrentPoint
.getX()) /
595 (aPreviousPoint
.getZ() - aCurrentPoint
.getZ()));
597 if(fTools::more(fCompare
, rPoint
.getX()))
607 // normal points mostly in Z-Direction, use XY-Polygon projection for check
609 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
611 const B3DPoint
aPreviousPoint(aCurrentPoint
);
612 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
615 const bool bCompYA(fTools::more(aPreviousPoint
.getY(), rPoint
.getY()));
616 const bool bCompYB(fTools::more(aCurrentPoint
.getY(), rPoint
.getY()));
618 if(bCompYA
!= bCompYB
)
621 const bool bCompXA(fTools::more(aPreviousPoint
.getX(), rPoint
.getX()));
622 const bool bCompXB(fTools::more(aCurrentPoint
.getX(), rPoint
.getX()));
624 if(bCompXA
== bCompXB
)
633 const double fCompare(
634 aCurrentPoint
.getX() - (aCurrentPoint
.getY() - rPoint
.getY()) *
635 (aPreviousPoint
.getX() - aCurrentPoint
.getX()) /
636 (aPreviousPoint
.getY() - aCurrentPoint
.getY()));
638 if(fTools::more(fCompare
, rPoint
.getX()))
653 bool isPointOnLine(const B3DPoint
& rStart
, const B3DPoint
& rEnd
, const B3DPoint
& rCandidate
, bool bWithPoints
)
655 if(rCandidate
.equal(rStart
) || rCandidate
.equal(rEnd
))
657 // candidate is in epsilon around start or end -> inside
660 else if(rStart
.equal(rEnd
))
662 // start and end are equal, but candidate is outside their epsilon -> outside
667 const B3DVector
aEdgeVector(rEnd
- rStart
);
668 const B3DVector
aTestVector(rCandidate
- rStart
);
670 if(areParallel(aEdgeVector
, aTestVector
))
672 double fParamTestOnCurr(0.0);
674 if(aEdgeVector
.getX() > aEdgeVector
.getY())
676 if(aEdgeVector
.getX() > aEdgeVector
.getZ())
679 fParamTestOnCurr
= aTestVector
.getX() / aEdgeVector
.getX();
684 fParamTestOnCurr
= aTestVector
.getZ() / aEdgeVector
.getZ();
689 if(aEdgeVector
.getY() > aEdgeVector
.getZ())
692 fParamTestOnCurr
= aTestVector
.getY() / aEdgeVector
.getY();
697 fParamTestOnCurr
= aTestVector
.getZ() / aEdgeVector
.getZ();
701 if(fParamTestOnCurr
> 0.0 && fTools::less(fParamTestOnCurr
, 1.0))
711 bool isPointOnPolygon(const B3DPolygon
& rCandidate
, const B3DPoint
& rPoint
)
713 const sal_uInt32
nPointCount(rCandidate
.count());
717 const sal_uInt32
nLoopCount(rCandidate
.isClosed() ? nPointCount
: nPointCount
- 1);
718 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(0));
720 for(sal_uInt32
a(0); a
< nLoopCount
; a
++)
722 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint((a
+ 1) % nPointCount
));
724 if(isPointOnLine(aCurrentPoint
, aNextPoint
, rPoint
, true/*bWithPoints*/))
729 aCurrentPoint
= aNextPoint
;
734 return rPoint
.equal(rCandidate
.getB3DPoint(0));
740 bool getCutBetweenLineAndPlane(const B3DVector
& rPlaneNormal
, const B3DPoint
& rPlanePoint
, const B3DPoint
& rEdgeStart
, const B3DPoint
& rEdgeEnd
, double& fCut
)
742 if(!rPlaneNormal
.equalZero() && !rEdgeStart
.equal(rEdgeEnd
))
744 const B3DVector
aTestEdge(rEdgeEnd
- rEdgeStart
);
745 const double fScalarEdge(rPlaneNormal
.scalar(aTestEdge
));
747 if(!fTools::equalZero(fScalarEdge
))
749 const B3DVector
aCompareEdge(rPlanePoint
- rEdgeStart
);
750 const double fScalarCompare(rPlaneNormal
.scalar(aCompareEdge
));
752 fCut
= fScalarCompare
/ fScalarEdge
;
760 // snap points of horizontal or vertical edges to discrete values
761 B3DPolygon
snapPointsOfHorizontalOrVerticalEdges(const B3DPolygon
& rCandidate
)
763 const sal_uInt32
nPointCount(rCandidate
.count());
767 // Start by copying the source polygon to get a writeable copy. The closed state is
768 // copied by aRetval's initialisation, too, so no need to copy it in this method
769 B3DPolygon
aRetval(rCandidate
);
771 // prepare geometry data. Get rounded from original
772 B3ITuple
aPrevTuple(basegfx::fround(rCandidate
.getB3DPoint(nPointCount
- 1)));
773 B3DPoint
aCurrPoint(rCandidate
.getB3DPoint(0));
774 B3ITuple
aCurrTuple(basegfx::fround(aCurrPoint
));
776 // loop over all points. This will also snap the implicit closing edge
777 // even when not closed, but that's no problem here
778 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
780 // get next point. Get rounded from original
781 const bool bLastRun(a
+ 1 == nPointCount
);
782 const sal_uInt32
nNextIndex(bLastRun
? 0 : a
+ 1);
783 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
784 const B3ITuple
aNextTuple(basegfx::fround(aNextPoint
));
787 const bool bPrevVertical(aPrevTuple
.getX() == aCurrTuple
.getX());
788 const bool bNextVertical(aNextTuple
.getX() == aCurrTuple
.getX());
789 const bool bPrevHorizontal(aPrevTuple
.getY() == aCurrTuple
.getY());
790 const bool bNextHorizontal(aNextTuple
.getY() == aCurrTuple
.getY());
791 const bool bSnapX(bPrevVertical
|| bNextVertical
);
792 const bool bSnapY(bPrevHorizontal
|| bNextHorizontal
);
796 const B3DPoint
aSnappedPoint(
797 bSnapX
? aCurrTuple
.getX() : aCurrPoint
.getX(),
798 bSnapY
? aCurrTuple
.getY() : aCurrPoint
.getY(),
801 aRetval
.setB3DPoint(a
, aSnappedPoint
);
804 // prepare next point
807 aPrevTuple
= aCurrTuple
;
808 aCurrPoint
= aNextPoint
;
809 aCurrTuple
= aNextTuple
;
821 } // end of namespace
823 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */