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/numeric/ftools.hxx>
24 #include <basegfx/range/b3drange.hxx>
25 #include <basegfx/point/b2dpoint.hxx>
26 #include <basegfx/matrix/b3dhommatrix.hxx>
27 #include <basegfx/polygon/b2dpolygon.hxx>
28 #include <basegfx/polygon/b2dpolygontools.hxx>
29 #include <basegfx/tuple/b3ituple.hxx>
32 //////////////////////////////////////////////////////////////////////////////
39 void checkClosed(B3DPolygon
& rCandidate
)
41 while(rCandidate
.count() > 1L
42 && rCandidate
.getB3DPoint(0L).equal(rCandidate
.getB3DPoint(rCandidate
.count() - 1L)))
44 rCandidate
.setClosed(true);
45 rCandidate
.remove(rCandidate
.count() - 1L);
49 sal_uInt32
getIndexOfSuccessor(sal_uInt32 nIndex
, const B3DPolygon
& rCandidate
)
51 OSL_ENSURE(nIndex
< rCandidate
.count(), "getIndexOfPredecessor: Access to polygon out of range (!)");
53 if(nIndex
+ 1L < rCandidate
.count())
63 B3DRange
getRange(const B3DPolygon
& rCandidate
)
66 const sal_uInt32
nPointCount(rCandidate
.count());
68 for(sal_uInt32
a(0L); a
< nPointCount
; a
++)
70 const B3DPoint
aTestPoint(rCandidate
.getB3DPoint(a
));
71 aRetval
.expand(aTestPoint
);
77 B3DVector
getNormal(const B3DPolygon
& rCandidate
)
79 return rCandidate
.getNormal();
82 double getLength(const B3DPolygon
& rCandidate
)
85 const sal_uInt32
nPointCount(rCandidate
.count());
89 const sal_uInt32
nLoopCount(rCandidate
.isClosed() ? nPointCount
: nPointCount
- 1L);
91 for(sal_uInt32
a(0L); a
< nLoopCount
; a
++)
93 const sal_uInt32
nNextIndex(getIndexOfSuccessor(a
, rCandidate
));
94 const B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(a
));
95 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
96 const B3DVector
aVector(aNextPoint
- aCurrentPoint
);
97 fRetval
+= aVector
.getLength();
104 void applyLineDashing(const B3DPolygon
& rCandidate
, const ::std::vector
<double>& rDotDashArray
, B3DPolyPolygon
* pLineTarget
, B3DPolyPolygon
* pGapTarget
, double fDotDashLength
)
106 const sal_uInt32
nPointCount(rCandidate
.count());
107 const sal_uInt32
nDotDashCount(rDotDashArray
.size());
109 if(fTools::lessOrEqual(fDotDashLength
, 0.0))
111 fDotDashLength
= ::std::accumulate(rDotDashArray
.begin(), rDotDashArray
.end(), 0.0);
114 if(fTools::more(fDotDashLength
, 0.0) && (pLineTarget
|| pGapTarget
) && nPointCount
)
119 pLineTarget
->clear();
127 // prepare current edge's start
128 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(0));
129 const sal_uInt32
nEdgeCount(rCandidate
.isClosed() ? nPointCount
: nPointCount
- 1);
131 // prepare DotDashArray iteration and the line/gap switching bool
132 sal_uInt32
nDotDashIndex(0);
134 double fDotDashMovingLength(rDotDashArray
[0]);
137 // iterate over all edges
138 for(sal_uInt32
a(0); a
< nEdgeCount
; a
++)
140 // update current edge
141 double fLastDotDashMovingLength(0.0);
142 const sal_uInt32
nNextIndex((a
+ 1) % nPointCount
);
143 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
144 const double fEdgeLength(B3DVector(aNextPoint
- aCurrentPoint
).getLength());
146 if(!fTools::equalZero(fEdgeLength
))
148 while(fTools::less(fDotDashMovingLength
, fEdgeLength
))
150 // new split is inside edge, create and append snippet [fLastDotDashMovingLength, fDotDashMovingLength]
151 const bool bHandleLine(bIsLine
&& pLineTarget
);
152 const bool bHandleGap(!bIsLine
&& pGapTarget
);
154 if(bHandleLine
|| bHandleGap
)
156 if(!aSnippet
.count())
158 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fLastDotDashMovingLength
/ fEdgeLength
));
161 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fDotDashMovingLength
/ fEdgeLength
));
165 pLineTarget
->append(aSnippet
);
169 pGapTarget
->append(aSnippet
);
175 // prepare next DotDashArray step and flip line/gap flag
176 fLastDotDashMovingLength
= fDotDashMovingLength
;
177 fDotDashMovingLength
+= rDotDashArray
[(++nDotDashIndex
) % nDotDashCount
];
181 // append snippet [fLastDotDashMovingLength, fEdgeLength]
182 const bool bHandleLine(bIsLine
&& pLineTarget
);
183 const bool bHandleGap(!bIsLine
&& pGapTarget
);
185 if(bHandleLine
|| bHandleGap
)
187 if(!aSnippet
.count())
189 aSnippet
.append(interpolate(aCurrentPoint
, aNextPoint
, fLastDotDashMovingLength
/ fEdgeLength
));
192 aSnippet
.append(aNextPoint
);
195 // prepare move to next edge
196 fDotDashMovingLength
-= fEdgeLength
;
199 // prepare next edge step (end point gets new start point)
200 aCurrentPoint
= aNextPoint
;
203 // append last intermediate results (if exists)
206 if(bIsLine
&& pLineTarget
)
208 pLineTarget
->append(aSnippet
);
210 else if(!bIsLine
&& pGapTarget
)
212 pGapTarget
->append(aSnippet
);
216 // check if start and end polygon may be merged
219 const sal_uInt32
nCount(pLineTarget
->count());
223 // these polygons were created above, there exists none with less than two points,
224 // thus dircet point access below is allowed
225 const B3DPolygon
aFirst(pLineTarget
->getB3DPolygon(0));
226 B3DPolygon
aLast(pLineTarget
->getB3DPolygon(nCount
- 1));
228 if(aFirst
.getB3DPoint(0).equal(aLast
.getB3DPoint(aLast
.count() - 1)))
230 // start of first and end of last are the same -> merge them
231 aLast
.append(aFirst
);
232 aLast
.removeDoublePoints();
233 pLineTarget
->setB3DPolygon(0, aLast
);
234 pLineTarget
->remove(nCount
- 1);
241 const sal_uInt32
nCount(pGapTarget
->count());
245 // these polygons were created above, there exists none with less than two points,
246 // thus dircet point access below is allowed
247 const B3DPolygon
aFirst(pGapTarget
->getB3DPolygon(0));
248 B3DPolygon
aLast(pGapTarget
->getB3DPolygon(nCount
- 1));
250 if(aFirst
.getB3DPoint(0).equal(aLast
.getB3DPoint(aLast
.count() - 1)))
252 // start of first and end of last are the same -> merge them
253 aLast
.append(aFirst
);
254 aLast
.removeDoublePoints();
255 pGapTarget
->setB3DPolygon(0, aLast
);
256 pGapTarget
->remove(nCount
- 1);
263 // parameters make no sense, just add source to targets
266 pLineTarget
->append(rCandidate
);
271 pGapTarget
->append(rCandidate
);
276 B3DPolygon
applyDefaultNormalsSphere( const B3DPolygon
& rCandidate
, const B3DPoint
& rCenter
)
278 B3DPolygon
aRetval(rCandidate
);
280 for(sal_uInt32
a(0L); a
< aRetval
.count(); a
++)
282 B3DVector
aVector(aRetval
.getB3DPoint(a
) - rCenter
);
284 aRetval
.setNormal(a
, aVector
);
290 B3DPolygon
invertNormals( const B3DPolygon
& rCandidate
)
292 B3DPolygon
aRetval(rCandidate
);
294 if(aRetval
.areNormalsUsed())
296 for(sal_uInt32
a(0L); a
< aRetval
.count(); a
++)
298 aRetval
.setNormal(a
, -aRetval
.getNormal(a
));
305 B3DPolygon
applyDefaultTextureCoordinatesParallel( const B3DPolygon
& rCandidate
, const B3DRange
& rRange
, bool bChangeX
, bool bChangeY
)
307 B3DPolygon
aRetval(rCandidate
);
309 if(bChangeX
|| bChangeY
)
311 // create projection of standard texture coordinates in (X, Y) onto
312 // the 3d coordinates straight
313 const double fWidth(rRange
.getWidth());
314 const double fHeight(rRange
.getHeight());
315 const bool bWidthSet(!fTools::equalZero(fWidth
));
316 const bool bHeightSet(!fTools::equalZero(fHeight
));
317 const double fOne(1.0);
319 for(sal_uInt32
a(0L); a
< aRetval
.count(); a
++)
321 const B3DPoint
aPoint(aRetval
.getB3DPoint(a
));
322 B2DPoint
aTextureCoordinate(aRetval
.getTextureCoordinate(a
));
328 aTextureCoordinate
.setX((aPoint
.getX() - rRange
.getMinX()) / fWidth
);
332 aTextureCoordinate
.setX(0.0);
340 aTextureCoordinate
.setY(fOne
- ((aPoint
.getY() - rRange
.getMinY()) / fHeight
));
344 aTextureCoordinate
.setY(fOne
);
348 aRetval
.setTextureCoordinate(a
, aTextureCoordinate
);
355 B3DPolygon
applyDefaultTextureCoordinatesSphere( const B3DPolygon
& rCandidate
, const B3DPoint
& rCenter
, bool bChangeX
, bool bChangeY
)
357 B3DPolygon
aRetval(rCandidate
);
359 if(bChangeX
|| bChangeY
)
361 // create texture coordinates using sphere projection to cartesian coordinates,
362 // use object's center as base
363 const double fOne(1.0);
364 const sal_uInt32
nPointCount(aRetval
.count());
365 bool bPolarPoints(false);
368 // create center cartesian coordinates to have a possibility to decide if on boundary
369 // transitions which value to choose
370 const B3DRange
aPlaneRange(getRange(rCandidate
));
371 const B3DPoint
aPlaneCenter(aPlaneRange
.getCenter() - rCenter
);
372 const double fXCenter(fOne
- ((atan2(aPlaneCenter
.getZ(), aPlaneCenter
.getX()) + F_PI
) / F_2PI
));
374 for(a
= 0L; a
< nPointCount
; a
++)
376 const B3DVector
aVector(aRetval
.getB3DPoint(a
) - rCenter
);
377 const double fY(fOne
- ((atan2(aVector
.getY(), aVector
.getXZLength()) + F_PI2
) / F_PI
));
378 B2DPoint
aTexCoor(aRetval
.getTextureCoordinate(a
));
380 if(fTools::equalZero(fY
))
382 // point is a north polar point, no useful X-coordinate can be created.
393 else if(fTools::equal(fY
, fOne
))
395 // point is a south polar point, no useful X-coordinate can be created. Set
396 // Y-coordinte, though
409 double fX(fOne
- ((atan2(aVector
.getZ(), aVector
.getX()) + F_PI
) / F_2PI
));
411 // correct cartesinan point coordiante dependent from center value
412 if(fX
> fXCenter
+ 0.5)
416 else if(fX
< fXCenter
- 0.5)
432 aRetval
.setTextureCoordinate(a
, aTexCoor
);
437 // correct X-texture coordinates if polar points are contained. Those
438 // coordinates cannot be correct, so use prev or next X-coordinate
439 for(a
= 0L; a
< nPointCount
; a
++)
441 B2DPoint
aTexCoor(aRetval
.getTextureCoordinate(a
));
443 if(fTools::equalZero(aTexCoor
.getY()) || fTools::equal(aTexCoor
.getY(), fOne
))
445 // get prev, next TexCoor and test for pole
446 const B2DPoint
aPrevTexCoor(aRetval
.getTextureCoordinate(a
? a
- 1L : nPointCount
- 1L));
447 const B2DPoint
aNextTexCoor(aRetval
.getTextureCoordinate((a
+ 1L) % nPointCount
));
448 const bool bPrevPole(fTools::equalZero(aPrevTexCoor
.getY()) || fTools::equal(aPrevTexCoor
.getY(), fOne
));
449 const bool bNextPole(fTools::equalZero(aNextTexCoor
.getY()) || fTools::equal(aNextTexCoor
.getY(), fOne
));
451 if(!bPrevPole
&& !bNextPole
)
453 // both no poles, mix them
454 aTexCoor
.setX((aPrevTexCoor
.getX() + aNextTexCoor
.getX()) / 2.0);
459 aTexCoor
.setX(aNextTexCoor
.getX());
463 // copy prev, even if it's a pole, hopefully it is already corrected
464 aTexCoor
.setX(aPrevTexCoor
.getX());
467 aRetval
.setTextureCoordinate(a
, aTexCoor
);
476 bool isInside(const B3DPolygon
& rCandidate
, const B3DPoint
& rPoint
, bool bWithBorder
)
478 if(bWithBorder
&& isPointOnPolygon(rCandidate
, rPoint
, true))
485 const B3DVector
aPlaneNormal(rCandidate
.getNormal());
487 if(!aPlaneNormal
.equalZero())
489 const sal_uInt32
nPointCount(rCandidate
.count());
493 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(nPointCount
- 1));
494 const double fAbsX(fabs(aPlaneNormal
.getX()));
495 const double fAbsY(fabs(aPlaneNormal
.getY()));
496 const double fAbsZ(fabs(aPlaneNormal
.getZ()));
498 if(fAbsX
> fAbsY
&& fAbsX
> fAbsZ
)
500 // normal points mostly in X-Direction, use YZ-Polygon projection for check
502 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
504 const B3DPoint
aPreviousPoint(aCurrentPoint
);
505 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
508 const bool bCompZA(fTools::more(aPreviousPoint
.getZ(), rPoint
.getZ()));
509 const bool bCompZB(fTools::more(aCurrentPoint
.getZ(), rPoint
.getZ()));
511 if(bCompZA
!= bCompZB
)
514 const bool bCompYA(fTools::more(aPreviousPoint
.getY(), rPoint
.getY()));
515 const bool bCompYB(fTools::more(aCurrentPoint
.getY(), rPoint
.getY()));
517 if(bCompYA
== bCompYB
)
526 const double fCompare(
527 aCurrentPoint
.getY() - (aCurrentPoint
.getZ() - rPoint
.getZ()) *
528 (aPreviousPoint
.getY() - aCurrentPoint
.getY()) /
529 (aPreviousPoint
.getZ() - aCurrentPoint
.getZ()));
531 if(fTools::more(fCompare
, rPoint
.getY()))
539 else if(fAbsY
> fAbsX
&& fAbsY
> fAbsZ
)
541 // normal points mostly in Y-Direction, use XZ-Polygon projection for check
543 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
545 const B3DPoint
aPreviousPoint(aCurrentPoint
);
546 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
549 const bool bCompZA(fTools::more(aPreviousPoint
.getZ(), rPoint
.getZ()));
550 const bool bCompZB(fTools::more(aCurrentPoint
.getZ(), rPoint
.getZ()));
552 if(bCompZA
!= bCompZB
)
555 const bool bCompXA(fTools::more(aPreviousPoint
.getX(), rPoint
.getX()));
556 const bool bCompXB(fTools::more(aCurrentPoint
.getX(), rPoint
.getX()));
558 if(bCompXA
== bCompXB
)
567 const double fCompare(
568 aCurrentPoint
.getX() - (aCurrentPoint
.getZ() - rPoint
.getZ()) *
569 (aPreviousPoint
.getX() - aCurrentPoint
.getX()) /
570 (aPreviousPoint
.getZ() - aCurrentPoint
.getZ()));
572 if(fTools::more(fCompare
, rPoint
.getX()))
582 // normal points mostly in Z-Direction, use XY-Polygon projection for check
584 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
586 const B3DPoint
aPreviousPoint(aCurrentPoint
);
587 aCurrentPoint
= rCandidate
.getB3DPoint(a
);
590 const bool bCompYA(fTools::more(aPreviousPoint
.getY(), rPoint
.getY()));
591 const bool bCompYB(fTools::more(aCurrentPoint
.getY(), rPoint
.getY()));
593 if(bCompYA
!= bCompYB
)
596 const bool bCompXA(fTools::more(aPreviousPoint
.getX(), rPoint
.getX()));
597 const bool bCompXB(fTools::more(aCurrentPoint
.getX(), rPoint
.getX()));
599 if(bCompXA
== bCompXB
)
608 const double fCompare(
609 aCurrentPoint
.getX() - (aCurrentPoint
.getY() - rPoint
.getY()) *
610 (aPreviousPoint
.getX() - aCurrentPoint
.getX()) /
611 (aPreviousPoint
.getY() - aCurrentPoint
.getY()));
613 if(fTools::more(fCompare
, rPoint
.getX()))
628 bool isPointOnLine(const B3DPoint
& rStart
, const B3DPoint
& rEnd
, const B3DPoint
& rCandidate
, bool bWithPoints
)
630 if(rCandidate
.equal(rStart
) || rCandidate
.equal(rEnd
))
632 // candidate is in epsilon around start or end -> inside
635 else if(rStart
.equal(rEnd
))
637 // start and end are equal, but candidate is outside their epsilon -> outside
642 const B3DVector
aEdgeVector(rEnd
- rStart
);
643 const B3DVector
aTestVector(rCandidate
- rStart
);
645 if(areParallel(aEdgeVector
, aTestVector
))
647 const double fZero(0.0);
648 const double fOne(1.0);
649 double fParamTestOnCurr(0.0);
651 if(aEdgeVector
.getX() > aEdgeVector
.getY())
653 if(aEdgeVector
.getX() > aEdgeVector
.getZ())
656 fParamTestOnCurr
= aTestVector
.getX() / aEdgeVector
.getX();
661 fParamTestOnCurr
= aTestVector
.getZ() / aEdgeVector
.getZ();
666 if(aEdgeVector
.getY() > aEdgeVector
.getZ())
669 fParamTestOnCurr
= aTestVector
.getY() / aEdgeVector
.getY();
674 fParamTestOnCurr
= aTestVector
.getZ() / aEdgeVector
.getZ();
678 if(fTools::more(fParamTestOnCurr
, fZero
) && fTools::less(fParamTestOnCurr
, fOne
))
688 bool isPointOnPolygon(const B3DPolygon
& rCandidate
, const B3DPoint
& rPoint
, bool bWithPoints
)
690 const sal_uInt32
nPointCount(rCandidate
.count());
694 const sal_uInt32
nLoopCount(rCandidate
.isClosed() ? nPointCount
: nPointCount
- 1L);
695 B3DPoint
aCurrentPoint(rCandidate
.getB3DPoint(0));
697 for(sal_uInt32
a(0); a
< nLoopCount
; a
++)
699 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint((a
+ 1) % nPointCount
));
701 if(isPointOnLine(aCurrentPoint
, aNextPoint
, rPoint
, bWithPoints
))
706 aCurrentPoint
= aNextPoint
;
709 else if(nPointCount
&& bWithPoints
)
711 return rPoint
.equal(rCandidate
.getB3DPoint(0));
717 bool getCutBetweenLineAndPlane(const B3DVector
& rPlaneNormal
, const B3DPoint
& rPlanePoint
, const B3DPoint
& rEdgeStart
, const B3DPoint
& rEdgeEnd
, double& fCut
)
719 if(!rPlaneNormal
.equalZero() && !rEdgeStart
.equal(rEdgeEnd
))
721 const B3DVector
aTestEdge(rEdgeEnd
- rEdgeStart
);
722 const double fScalarEdge(rPlaneNormal
.scalar(aTestEdge
));
724 if(!fTools::equalZero(fScalarEdge
))
726 const B3DVector
aCompareEdge(rPlanePoint
- rEdgeStart
);
727 const double fScalarCompare(rPlaneNormal
.scalar(aCompareEdge
));
729 fCut
= fScalarCompare
/ fScalarEdge
;
737 //////////////////////////////////////////////////////////////////////
738 // comparators with tolerance for 3D Polygons
740 bool equal(const B3DPolygon
& rCandidateA
, const B3DPolygon
& rCandidateB
, const double& rfSmallValue
)
742 const sal_uInt32
nPointCount(rCandidateA
.count());
744 if(nPointCount
!= rCandidateB
.count())
747 const bool bClosed(rCandidateA
.isClosed());
749 if(bClosed
!= rCandidateB
.isClosed())
752 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
754 const B3DPoint
aPoint(rCandidateA
.getB3DPoint(a
));
756 if(!aPoint
.equal(rCandidateB
.getB3DPoint(a
), rfSmallValue
))
763 // snap points of horizontal or vertical edges to discrete values
764 B3DPolygon
snapPointsOfHorizontalOrVerticalEdges(const B3DPolygon
& rCandidate
)
766 const sal_uInt32
nPointCount(rCandidate
.count());
770 // Start by copying the source polygon to get a writeable copy. The closed state is
771 // copied by aRetval's initialisation, too, so no need to copy it in this method
772 B3DPolygon
aRetval(rCandidate
);
774 // prepare geometry data. Get rounded from original
775 B3ITuple
aPrevTuple(basegfx::fround(rCandidate
.getB3DPoint(nPointCount
- 1)));
776 B3DPoint
aCurrPoint(rCandidate
.getB3DPoint(0));
777 B3ITuple
aCurrTuple(basegfx::fround(aCurrPoint
));
779 // loop over all points. This will also snap the implicit closing edge
780 // even when not closed, but that's no problem here
781 for(sal_uInt32
a(0); a
< nPointCount
; a
++)
783 // get next point. Get rounded from original
784 const bool bLastRun(a
+ 1 == nPointCount
);
785 const sal_uInt32
nNextIndex(bLastRun
? 0 : a
+ 1);
786 const B3DPoint
aNextPoint(rCandidate
.getB3DPoint(nNextIndex
));
787 const B3ITuple
aNextTuple(basegfx::fround(aNextPoint
));
790 const bool bPrevVertical(aPrevTuple
.getX() == aCurrTuple
.getX());
791 const bool bNextVertical(aNextTuple
.getX() == aCurrTuple
.getX());
792 const bool bPrevHorizontal(aPrevTuple
.getY() == aCurrTuple
.getY());
793 const bool bNextHorizontal(aNextTuple
.getY() == aCurrTuple
.getY());
794 const bool bSnapX(bPrevVertical
|| bNextVertical
);
795 const bool bSnapY(bPrevHorizontal
|| bNextHorizontal
);
799 const B3DPoint
aSnappedPoint(
800 bSnapX
? aCurrTuple
.getX() : aCurrPoint
.getX(),
801 bSnapY
? aCurrTuple
.getY() : aCurrPoint
.getY(),
804 aRetval
.setB3DPoint(a
, aSnappedPoint
);
807 // prepare next point
810 aPrevTuple
= aCurrTuple
;
811 aCurrPoint
= aNextPoint
;
812 aCurrTuple
= aNextTuple
;
824 } // end of namespace tools
825 } // end of namespace basegfx
827 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */