Line data Source code
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 <drawinglayer/primitive3d/sdrextrudeprimitive3d.hxx>
21 : #include <basegfx/matrix/b2dhommatrix.hxx>
22 : #include <basegfx/polygon/b2dpolygontools.hxx>
23 : #include <basegfx/polygon/b3dpolypolygontools.hxx>
24 : #include <drawinglayer/primitive3d/sdrdecompositiontools3d.hxx>
25 : #include <basegfx/tools/canvastools.hxx>
26 : #include <drawinglayer/primitive3d/drawinglayer_primitivetypes3d.hxx>
27 : #include <drawinglayer/geometry/viewinformation3d.hxx>
28 : #include <drawinglayer/attribute/sdrfillattribute.hxx>
29 : #include <drawinglayer/attribute/sdrlineattribute.hxx>
30 : #include <drawinglayer/attribute/sdrshadowattribute.hxx>
31 :
32 : //////////////////////////////////////////////////////////////////////////////
33 :
34 : using namespace com::sun::star;
35 :
36 : //////////////////////////////////////////////////////////////////////////////
37 :
38 : namespace drawinglayer
39 : {
40 : namespace primitive3d
41 : {
42 0 : Primitive3DSequence SdrExtrudePrimitive3D::create3DDecomposition(const geometry::ViewInformation3D& rViewInformation) const
43 : {
44 0 : Primitive3DSequence aRetval;
45 :
46 : // get slices
47 0 : const Slice3DVector& rSliceVector = getSlices();
48 :
49 0 : if(!rSliceVector.empty())
50 : {
51 : sal_uInt32 a;
52 :
53 : // decide what to create
54 0 : const ::com::sun::star::drawing::NormalsKind eNormalsKind(getSdr3DObjectAttribute().getNormalsKind());
55 0 : const bool bCreateNormals(::com::sun::star::drawing::NormalsKind_SPECIFIC == eNormalsKind);
56 0 : const bool bCreateTextureCoordiantesX(::com::sun::star::drawing::TextureProjectionMode_OBJECTSPECIFIC == getSdr3DObjectAttribute().getTextureProjectionX());
57 0 : const bool bCreateTextureCoordiantesY(::com::sun::star::drawing::TextureProjectionMode_OBJECTSPECIFIC == getSdr3DObjectAttribute().getTextureProjectionY());
58 0 : basegfx::B2DHomMatrix aTexTransform;
59 :
60 0 : if(!getSdrLFSAttribute().getFill().isDefault() && (bCreateTextureCoordiantesX || bCreateTextureCoordiantesY))
61 : {
62 0 : const basegfx::B2DPolygon aFirstPolygon(maCorrectedPolyPolygon.getB2DPolygon(0L));
63 0 : const double fFrontLength(basegfx::tools::getLength(aFirstPolygon));
64 0 : const double fFrontArea(basegfx::tools::getArea(aFirstPolygon));
65 0 : const double fSqrtFrontArea(sqrt(fFrontArea));
66 0 : double fRelativeTextureWidth = basegfx::fTools::equalZero(fSqrtFrontArea) ? 1.0 : fFrontLength / fSqrtFrontArea;
67 0 : fRelativeTextureWidth = (double)((sal_uInt32)(fRelativeTextureWidth - 0.5));
68 :
69 0 : if(fRelativeTextureWidth < 1.0)
70 : {
71 0 : fRelativeTextureWidth = 1.0;
72 : }
73 :
74 0 : aTexTransform.translate(-0.5, -0.5);
75 0 : aTexTransform.scale(-1.0, -1.0);
76 0 : aTexTransform.translate(0.5, 0.5);
77 0 : aTexTransform.scale(fRelativeTextureWidth, 1.0);
78 : }
79 :
80 : // create geometry
81 0 : ::std::vector< basegfx::B3DPolyPolygon > aFill;
82 : extractPlanesFromSlice(aFill, rSliceVector,
83 0 : bCreateNormals, getSmoothHorizontalNormals(), getSmoothNormals(), getSmoothLids(), false,
84 0 : 0.5, 0.6, bCreateTextureCoordiantesX || bCreateTextureCoordiantesY, aTexTransform);
85 :
86 : // get full range
87 0 : const basegfx::B3DRange aRange(getRangeFrom3DGeometry(aFill));
88 :
89 : // normal creation
90 0 : if(!getSdrLFSAttribute().getFill().isDefault())
91 : {
92 0 : if(::com::sun::star::drawing::NormalsKind_SPHERE == eNormalsKind)
93 : {
94 0 : applyNormalsKindSphereTo3DGeometry(aFill, aRange);
95 : }
96 0 : else if(::com::sun::star::drawing::NormalsKind_FLAT == eNormalsKind)
97 : {
98 0 : applyNormalsKindFlatTo3DGeometry(aFill);
99 : }
100 :
101 0 : if(getSdr3DObjectAttribute().getNormalsInvert())
102 : {
103 0 : applyNormalsInvertTo3DGeometry(aFill);
104 : }
105 : }
106 :
107 : // texture coordinates
108 0 : if(!getSdrLFSAttribute().getFill().isDefault())
109 : {
110 : applyTextureTo3DGeometry(
111 : getSdr3DObjectAttribute().getTextureProjectionX(),
112 : getSdr3DObjectAttribute().getTextureProjectionY(),
113 : aFill,
114 : aRange,
115 0 : getTextureSize());
116 : }
117 :
118 0 : if(!getSdrLFSAttribute().getFill().isDefault())
119 : {
120 : // add fill
121 : aRetval = create3DPolyPolygonFillPrimitives(
122 : aFill,
123 0 : getTransform(),
124 0 : getTextureSize(),
125 : getSdr3DObjectAttribute(),
126 0 : getSdrLFSAttribute().getFill(),
127 0 : getSdrLFSAttribute().getFillFloatTransGradient());
128 : }
129 : else
130 : {
131 : // create simplified 3d hit test geometry
132 : aRetval = createHiddenGeometryPrimitives3D(
133 : aFill,
134 0 : getTransform(),
135 0 : getTextureSize(),
136 0 : getSdr3DObjectAttribute());
137 : }
138 :
139 : // add line
140 0 : if(!getSdrLFSAttribute().getLine().isDefault())
141 : {
142 0 : if(getSdr3DObjectAttribute().getReducedLineGeometry())
143 : {
144 : // create geometric outlines with reduced line geometry for chart.
145 0 : const basegfx::B3DPolyPolygon aVerLine(extractVerticalLinesFromSlice(rSliceVector));
146 0 : const sal_uInt32 nCount(aVerLine.count());
147 0 : basegfx::B3DPolyPolygon aReducedLoops;
148 0 : basegfx::B3DPolyPolygon aNewLineGeometry;
149 :
150 : // sort out doubles (front and back planes when no edge rounding is done). Since
151 : // this is a line geometry merged from PolyPolygons, loop over all Polygons
152 0 : for(a = 0; a < nCount; a++)
153 : {
154 0 : const sal_uInt32 nReducedCount(aReducedLoops.count());
155 0 : const basegfx::B3DPolygon aCandidate(aVerLine.getB3DPolygon(a));
156 0 : bool bAdd(true);
157 :
158 0 : if(nReducedCount)
159 : {
160 0 : for(sal_uInt32 b(0); bAdd && b < nReducedCount; b++)
161 : {
162 0 : if(aCandidate == aReducedLoops.getB3DPolygon(b))
163 : {
164 0 : bAdd = false;
165 : }
166 : }
167 : }
168 :
169 0 : if(bAdd)
170 : {
171 0 : aReducedLoops.append(aCandidate);
172 : }
173 0 : }
174 :
175 : // from here work with reduced loops and reduced count without changing them
176 0 : const sal_uInt32 nReducedCount(aReducedLoops.count());
177 :
178 0 : if(nReducedCount > 1)
179 : {
180 0 : for(sal_uInt32 b(1); b < nReducedCount; b++)
181 : {
182 : // get loop pair
183 0 : const basegfx::B3DPolygon aCandA(aReducedLoops.getB3DPolygon(b - 1));
184 0 : const basegfx::B3DPolygon aCandB(aReducedLoops.getB3DPolygon(b));
185 :
186 : // for each loop pair create the connection edges
187 : createReducedOutlines(
188 : rViewInformation,
189 0 : getTransform(),
190 : aCandA,
191 : aCandB,
192 0 : aNewLineGeometry);
193 0 : }
194 : }
195 :
196 : // add reduced loops themselves
197 0 : aNewLineGeometry.append(aReducedLoops);
198 :
199 : // to create vertical edges at non-C1/C2 steady loops, use maCorrectedPolyPolygon
200 : // directly since the 3D Polygons do not suport this.
201 : //
202 : // Unfortunately there is no bezier polygon provided by the chart module; one reason is
203 : // that the API for extrude wants a 3D polygon geometry (for historical reasons, i guess)
204 : // and those have no beziers. Another reason is that he chart module uses self-created
205 : // stuff to create the 2D geometry (in ShapeFactory::createPieSegment), but this geometry
206 : // does not contain bezier infos, either. The only way which is possible for now is to 'detect'
207 : // candidates for vertical edges of pie segments by looking for the angles in the polygon.
208 : //
209 : // This is all not very well designed ATM. Ideally, the ReducedLineGeometry is responsible
210 : // for creating the outer geometry edges (createReducedOutlines), but for special edges
211 : // like the vertical ones for pie center and both start/end, the incarnation with the
212 : // knowledge about that it needs to create those and IS a pie segment -> in this case,
213 : // the chart itself.
214 0 : const sal_uInt32 nPolyCount(maCorrectedPolyPolygon.count());
215 :
216 0 : for(sal_uInt32 c(0); c < nPolyCount; c++)
217 : {
218 0 : const basegfx::B2DPolygon aCandidate(maCorrectedPolyPolygon.getB2DPolygon(c));
219 0 : const sal_uInt32 nPointCount(aCandidate.count());
220 :
221 0 : if(nPointCount > 2)
222 : {
223 0 : sal_uInt32 nIndexA(nPointCount);
224 0 : sal_uInt32 nIndexB(nPointCount);
225 0 : sal_uInt32 nIndexC(nPointCount);
226 :
227 0 : for(sal_uInt32 d(0); d < nPointCount; d++)
228 : {
229 0 : const sal_uInt32 nPrevInd((d + nPointCount - 1) % nPointCount);
230 0 : const sal_uInt32 nNextInd((d + 1) % nPointCount);
231 0 : const basegfx::B2DPoint aPoint(aCandidate.getB2DPoint(d));
232 0 : const basegfx::B2DVector aPrev(aCandidate.getB2DPoint(nPrevInd) - aPoint);
233 0 : const basegfx::B2DVector aNext(aCandidate.getB2DPoint(nNextInd) - aPoint);
234 0 : const double fAngle(aPrev.angle(aNext));
235 :
236 : // take each angle which deviates more than 10% from going straight as
237 : // special edge. This will detect the two outer edges of pie segments,
238 : // but not always the center one (think about a near 180 degree pie)
239 0 : if(F_PI - fabs(fAngle) > F_PI * 0.1)
240 : {
241 0 : if(nPointCount == nIndexA)
242 : {
243 0 : nIndexA = d;
244 : }
245 0 : else if(nPointCount == nIndexB)
246 : {
247 0 : nIndexB = d;
248 : }
249 0 : else if(nPointCount == nIndexC)
250 : {
251 0 : nIndexC = d;
252 0 : d = nPointCount;
253 : }
254 : }
255 0 : }
256 :
257 0 : const bool bIndexAUsed(nIndexA != nPointCount);
258 0 : const bool bIndexBUsed(nIndexB != nPointCount);
259 0 : bool bIndexCUsed(nIndexC != nPointCount);
260 :
261 0 : if(bIndexCUsed)
262 : {
263 : // already three special edges found, so the center one was already detected
264 : // and does not need to be searched
265 : }
266 0 : else if(bIndexAUsed && bIndexBUsed)
267 : {
268 : // outer edges detected (they are approx. 90 degrees), but center one not.
269 : // Look with the knowledge that it's in-between the two found ones
270 0 : if(((nIndexA + 2) % nPointCount) == nIndexB)
271 : {
272 0 : nIndexC = (nIndexA + 1) % nPointCount;
273 : }
274 0 : else if(((nIndexA + nPointCount - 2) % nPointCount) == nIndexB)
275 : {
276 0 : nIndexC = (nIndexA + nPointCount - 1) % nPointCount;
277 : }
278 :
279 0 : bIndexCUsed = (nIndexC != nPointCount);
280 : }
281 :
282 0 : if(bIndexAUsed)
283 : {
284 0 : const basegfx::B2DPoint aPoint(aCandidate.getB2DPoint(nIndexA));
285 0 : const basegfx::B3DPoint aStart(aPoint.getX(), aPoint.getY(), 0.0);
286 0 : const basegfx::B3DPoint aEnd(aPoint.getX(), aPoint.getY(), getDepth());
287 0 : basegfx::B3DPolygon aToBeAdded;
288 :
289 0 : aToBeAdded.append(aStart);
290 0 : aToBeAdded.append(aEnd);
291 0 : aNewLineGeometry.append(aToBeAdded);
292 : }
293 :
294 0 : if(bIndexBUsed)
295 : {
296 0 : const basegfx::B2DPoint aPoint(aCandidate.getB2DPoint(nIndexB));
297 0 : const basegfx::B3DPoint aStart(aPoint.getX(), aPoint.getY(), 0.0);
298 0 : const basegfx::B3DPoint aEnd(aPoint.getX(), aPoint.getY(), getDepth());
299 0 : basegfx::B3DPolygon aToBeAdded;
300 :
301 0 : aToBeAdded.append(aStart);
302 0 : aToBeAdded.append(aEnd);
303 0 : aNewLineGeometry.append(aToBeAdded);
304 : }
305 :
306 0 : if(bIndexCUsed)
307 : {
308 0 : const basegfx::B2DPoint aPoint(aCandidate.getB2DPoint(nIndexC));
309 0 : const basegfx::B3DPoint aStart(aPoint.getX(), aPoint.getY(), 0.0);
310 0 : const basegfx::B3DPoint aEnd(aPoint.getX(), aPoint.getY(), getDepth());
311 0 : basegfx::B3DPolygon aToBeAdded;
312 :
313 0 : aToBeAdded.append(aStart);
314 0 : aToBeAdded.append(aEnd);
315 0 : aNewLineGeometry.append(aToBeAdded);
316 : }
317 : }
318 0 : }
319 :
320 : // append loops themselves
321 0 : aNewLineGeometry.append(aReducedLoops);
322 :
323 0 : if(aNewLineGeometry.count())
324 : {
325 : const Primitive3DSequence aLines(create3DPolyPolygonLinePrimitives(
326 0 : aNewLineGeometry, getTransform(), getSdrLFSAttribute().getLine()));
327 0 : appendPrimitive3DSequenceToPrimitive3DSequence(aRetval, aLines);
328 0 : }
329 : }
330 : else
331 : {
332 : // extract line geometry from slices
333 0 : const basegfx::B3DPolyPolygon aHorLine(extractHorizontalLinesFromSlice(rSliceVector, false));
334 0 : const basegfx::B3DPolyPolygon aVerLine(extractVerticalLinesFromSlice(rSliceVector));
335 :
336 : // add horizontal lines
337 : const Primitive3DSequence aHorLines(create3DPolyPolygonLinePrimitives(
338 0 : aHorLine, getTransform(), getSdrLFSAttribute().getLine()));
339 0 : appendPrimitive3DSequenceToPrimitive3DSequence(aRetval, aHorLines);
340 :
341 : // add vertical lines
342 : const Primitive3DSequence aVerLines(create3DPolyPolygonLinePrimitives(
343 0 : aVerLine, getTransform(), getSdrLFSAttribute().getLine()));
344 0 : appendPrimitive3DSequenceToPrimitive3DSequence(aRetval, aVerLines);
345 : }
346 : }
347 :
348 : // add shadow
349 0 : if(!getSdrLFSAttribute().getShadow().isDefault() && aRetval.hasElements())
350 : {
351 : const Primitive3DSequence aShadow(createShadowPrimitive3D(
352 0 : aRetval, getSdrLFSAttribute().getShadow(), getSdr3DObjectAttribute().getShadow3D()));
353 0 : appendPrimitive3DSequenceToPrimitive3DSequence(aRetval, aShadow);
354 0 : }
355 : }
356 :
357 0 : return aRetval;
358 : }
359 :
360 0 : void SdrExtrudePrimitive3D::impCreateSlices()
361 : {
362 : // prepare the polygon. No double points, correct orientations and a correct
363 : // outmost polygon are needed
364 : // Also important: subdivide here to ensure equal point count for all slices (!)
365 0 : maCorrectedPolyPolygon = basegfx::tools::adaptiveSubdivideByAngle(getPolyPolygon());
366 0 : maCorrectedPolyPolygon.removeDoublePoints();
367 0 : maCorrectedPolyPolygon = basegfx::tools::correctOrientations(maCorrectedPolyPolygon);
368 0 : maCorrectedPolyPolygon = basegfx::tools::correctOutmostPolygon(maCorrectedPolyPolygon);
369 :
370 : // prepare slices as geometry
371 0 : createExtrudeSlices(maSlices, maCorrectedPolyPolygon, getBackScale(), getDiagonal(), getDepth(), getCharacterMode(), getCloseFront(), getCloseBack());
372 0 : }
373 :
374 0 : const Slice3DVector& SdrExtrudePrimitive3D::getSlices() const
375 : {
376 : // This can be made dependent of getSdrLFSAttribute().getFill() and getSdrLFSAttribute().getLine()
377 : // again when no longer geometry is needed for non-visible 3D objects as it is now for chart
378 0 : if(getPolyPolygon().count() && !maSlices.size())
379 : {
380 0 : ::osl::Mutex m_mutex;
381 0 : const_cast< SdrExtrudePrimitive3D& >(*this).impCreateSlices();
382 : }
383 :
384 0 : return maSlices;
385 : }
386 :
387 0 : SdrExtrudePrimitive3D::SdrExtrudePrimitive3D(
388 : const basegfx::B3DHomMatrix& rTransform,
389 : const basegfx::B2DVector& rTextureSize,
390 : const attribute::SdrLineFillShadowAttribute3D& rSdrLFSAttribute,
391 : const attribute::Sdr3DObjectAttribute& rSdr3DObjectAttribute,
392 : const basegfx::B2DPolyPolygon& rPolyPolygon,
393 : double fDepth,
394 : double fDiagonal,
395 : double fBackScale,
396 : bool bSmoothNormals,
397 : bool bSmoothHorizontalNormals,
398 : bool bSmoothLids,
399 : bool bCharacterMode,
400 : bool bCloseFront,
401 : bool bCloseBack)
402 : : SdrPrimitive3D(rTransform, rTextureSize, rSdrLFSAttribute, rSdr3DObjectAttribute),
403 : maCorrectedPolyPolygon(),
404 : maSlices(),
405 : maPolyPolygon(rPolyPolygon),
406 : mfDepth(fDepth),
407 : mfDiagonal(fDiagonal),
408 : mfBackScale(fBackScale),
409 : mpLastRLGViewInformation(0),
410 : mbSmoothNormals(bSmoothNormals),
411 : mbSmoothHorizontalNormals(bSmoothHorizontalNormals),
412 : mbSmoothLids(bSmoothLids),
413 : mbCharacterMode(bCharacterMode),
414 : mbCloseFront(bCloseFront),
415 0 : mbCloseBack(bCloseBack)
416 : {
417 : // make sure depth is positive
418 0 : if(basegfx::fTools::lessOrEqual(getDepth(), 0.0))
419 : {
420 0 : mfDepth = 0.0;
421 : }
422 :
423 : // make sure the percentage value getDiagonal() is between 0.0 and 1.0
424 0 : if(basegfx::fTools::lessOrEqual(getDiagonal(), 0.0))
425 : {
426 0 : mfDiagonal = 0.0;
427 : }
428 0 : else if(basegfx::fTools::moreOrEqual(getDiagonal(), 1.0))
429 : {
430 0 : mfDiagonal = 1.0;
431 : }
432 :
433 : // no close front/back when polygon is not closed
434 0 : if(getPolyPolygon().count() && !getPolyPolygon().getB2DPolygon(0L).isClosed())
435 : {
436 0 : mbCloseFront = mbCloseBack = false;
437 : }
438 :
439 : // no edge rounding when not closing
440 0 : if(!getCloseFront() && !getCloseBack())
441 : {
442 0 : mfDiagonal = 0.0;
443 : }
444 0 : }
445 :
446 0 : SdrExtrudePrimitive3D::~SdrExtrudePrimitive3D()
447 : {
448 0 : if(mpLastRLGViewInformation)
449 : {
450 0 : delete mpLastRLGViewInformation;
451 : }
452 0 : }
453 :
454 0 : bool SdrExtrudePrimitive3D::operator==(const BasePrimitive3D& rPrimitive) const
455 : {
456 0 : if(SdrPrimitive3D::operator==(rPrimitive))
457 : {
458 0 : const SdrExtrudePrimitive3D& rCompare = static_cast< const SdrExtrudePrimitive3D& >(rPrimitive);
459 :
460 0 : return (getPolyPolygon() == rCompare.getPolyPolygon()
461 0 : && getDepth() == rCompare.getDepth()
462 0 : && getDiagonal() == rCompare.getDiagonal()
463 0 : && getBackScale() == rCompare.getBackScale()
464 0 : && getSmoothNormals() == rCompare.getSmoothNormals()
465 0 : && getSmoothHorizontalNormals() == rCompare.getSmoothHorizontalNormals()
466 0 : && getSmoothLids() == rCompare.getSmoothLids()
467 0 : && getCharacterMode() == rCompare.getCharacterMode()
468 0 : && getCloseFront() == rCompare.getCloseFront()
469 0 : && getCloseBack() == rCompare.getCloseBack());
470 : }
471 :
472 0 : return false;
473 : }
474 :
475 0 : basegfx::B3DRange SdrExtrudePrimitive3D::getB3DRange(const geometry::ViewInformation3D& /*rViewInformation*/) const
476 : {
477 : // use defaut from sdrPrimitive3D which uses transformation expanded by line width/2
478 : // The parent implementation which uses the ranges of the decomposition would be more
479 : // corrcet, but for historical reasons it is necessary to do the old method: To get
480 : // the range of the non-transformed geometry and transform it then. This leads to different
481 : // ranges where the new method is more correct, but the need to keep the old behaviour
482 : // has priority here.
483 0 : return get3DRangeFromSlices(getSlices());
484 : }
485 :
486 0 : Primitive3DSequence SdrExtrudePrimitive3D::get3DDecomposition(const geometry::ViewInformation3D& rViewInformation) const
487 : {
488 0 : if(getSdr3DObjectAttribute().getReducedLineGeometry())
489 : {
490 0 : if(!mpLastRLGViewInformation ||
491 0 : (getBuffered3DDecomposition().hasElements()
492 0 : && *mpLastRLGViewInformation != rViewInformation))
493 : {
494 : // conditions of last local decomposition with reduced lines have changed. Remember
495 : // new one and clear current decompositiopn
496 0 : ::osl::Mutex m_mutex;
497 0 : SdrExtrudePrimitive3D* pThat = const_cast< SdrExtrudePrimitive3D* >(this);
498 0 : pThat->setBuffered3DDecomposition(Primitive3DSequence());
499 0 : delete pThat->mpLastRLGViewInformation;
500 0 : pThat->mpLastRLGViewInformation = new geometry::ViewInformation3D(rViewInformation);
501 : }
502 : }
503 :
504 : // no test for buffering needed, call parent
505 0 : return SdrPrimitive3D::get3DDecomposition(rViewInformation);
506 : }
507 :
508 : // provide unique ID
509 0 : ImplPrimitrive3DIDBlock(SdrExtrudePrimitive3D, PRIMITIVE3D_ID_SDREXTRUDEPRIMITIVE3D)
510 :
511 : } // end of namespace primitive3d
512 : } // end of namespace drawinglayer
513 :
514 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|