Teach symstore more duplicated DLLs
[LibreOffice.git] / drawinglayer / source / primitive2d / polygonprimitive2d.cxx
blobbcf1fc6bbc3b214ebaa3accb0352c2b7e978359b
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
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 <drawinglayer/primitive2d/polygonprimitive2d.hxx>
21 #include <basegfx/utils/canvastools.hxx>
22 #include <basegfx/polygon/b2dpolygontools.hxx>
23 #include <basegfx/polygon/b2dpolypolygontools.hxx>
24 #include <drawinglayer/primitive2d/polypolygonprimitive2d.hxx>
25 #include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
26 #include <drawinglayer/geometry/viewinformation2d.hxx>
27 #include <basegfx/polygon/b2dlinegeometry.hxx>
28 #include <com/sun/star/drawing/LineCap.hpp>
29 #include <comphelper/random.hxx>
31 #include <converters.hxx>
33 using namespace com::sun::star;
34 using namespace std;
37 namespace drawinglayer
39 namespace primitive2d
41 PolygonHairlinePrimitive2D::PolygonHairlinePrimitive2D(
42 const basegfx::B2DPolygon& rPolygon,
43 const basegfx::BColor& rBColor)
44 : BasePrimitive2D(),
45 maPolygon(rPolygon),
46 maBColor(rBColor)
50 bool PolygonHairlinePrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
52 if(BasePrimitive2D::operator==(rPrimitive))
54 const PolygonHairlinePrimitive2D& rCompare = static_cast<const PolygonHairlinePrimitive2D&>(rPrimitive);
56 return (getB2DPolygon() == rCompare.getB2DPolygon()
57 && getBColor() == rCompare.getBColor());
60 return false;
63 basegfx::B2DRange PolygonHairlinePrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
65 // this is a hairline, thus the line width is view-dependent. Get range of polygon
66 // as base size
67 basegfx::B2DRange aRetval(getB2DPolygon().getB2DRange());
69 if(!aRetval.isEmpty())
71 // Calculate view-dependent hairline width
72 const basegfx::B2DVector aDiscreteSize(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0));
73 const double fDiscreteHalfLineWidth(aDiscreteSize.getLength() * 0.5);
75 if(basegfx::fTools::more(fDiscreteHalfLineWidth, 0.0))
77 aRetval.grow(fDiscreteHalfLineWidth);
81 // return range
82 return aRetval;
85 // provide unique ID
86 ImplPrimitive2DIDBlock(PolygonHairlinePrimitive2D, PRIMITIVE2D_ID_POLYGONHAIRLINEPRIMITIVE2D)
88 } // end of namespace primitive2d
89 } // end of namespace drawinglayer
92 namespace drawinglayer
94 namespace primitive2d
96 void PolygonMarkerPrimitive2D::create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
98 // calculate logic DashLength
99 const basegfx::B2DVector aDashVector(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(getDiscreteDashLength(), 0.0));
100 const double fLogicDashLength(aDashVector.getX());
102 if(fLogicDashLength > 0.0 && !getRGBColorA().equal(getRGBColorB()))
104 // apply dashing; get line and gap snippets
105 std::vector< double > aDash;
106 basegfx::B2DPolyPolygon aDashedPolyPolyA;
107 basegfx::B2DPolyPolygon aDashedPolyPolyB;
109 aDash.push_back(fLogicDashLength);
110 aDash.push_back(fLogicDashLength);
111 basegfx::utils::applyLineDashing(getB2DPolygon(), aDash, &aDashedPolyPolyA, &aDashedPolyPolyB, 2.0 * fLogicDashLength);
113 rContainer.push_back(new PolyPolygonHairlinePrimitive2D(aDashedPolyPolyA, getRGBColorA()));
114 rContainer.push_back(new PolyPolygonHairlinePrimitive2D(aDashedPolyPolyB, getRGBColorB()));
116 else
118 rContainer.push_back(new PolygonHairlinePrimitive2D(getB2DPolygon(), getRGBColorA()));
122 PolygonMarkerPrimitive2D::PolygonMarkerPrimitive2D(
123 const basegfx::B2DPolygon& rPolygon,
124 const basegfx::BColor& rRGBColorA,
125 const basegfx::BColor& rRGBColorB,
126 double fDiscreteDashLength)
127 : BufferedDecompositionPrimitive2D(),
128 maPolygon(rPolygon),
129 maRGBColorA(rRGBColorA),
130 maRGBColorB(rRGBColorB),
131 mfDiscreteDashLength(fDiscreteDashLength),
132 maLastInverseObjectToViewTransformation()
136 bool PolygonMarkerPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
138 if(BufferedDecompositionPrimitive2D::operator==(rPrimitive))
140 const PolygonMarkerPrimitive2D& rCompare = static_cast<const PolygonMarkerPrimitive2D&>(rPrimitive);
142 return (getB2DPolygon() == rCompare.getB2DPolygon()
143 && getRGBColorA() == rCompare.getRGBColorA()
144 && getRGBColorB() == rCompare.getRGBColorB()
145 && getDiscreteDashLength() == rCompare.getDiscreteDashLength());
148 return false;
151 basegfx::B2DRange PolygonMarkerPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
153 // this is a hairline, thus the line width is view-dependent. Get range of polygon
154 // as base size
155 basegfx::B2DRange aRetval(getB2DPolygon().getB2DRange());
157 if(!aRetval.isEmpty())
159 // Calculate view-dependent hairline width
160 const basegfx::B2DVector aDiscreteSize(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0));
161 const double fDiscreteHalfLineWidth(aDiscreteSize.getLength() * 0.5);
163 if(basegfx::fTools::more(fDiscreteHalfLineWidth, 0.0))
165 aRetval.grow(fDiscreteHalfLineWidth);
169 // return range
170 return aRetval;
173 void PolygonMarkerPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor, const geometry::ViewInformation2D& rViewInformation) const
175 ::osl::MutexGuard aGuard( m_aMutex );
176 bool bNeedNewDecomposition(false);
178 if(!getBuffered2DDecomposition().empty())
180 if(rViewInformation.getInverseObjectToViewTransformation() != maLastInverseObjectToViewTransformation)
182 bNeedNewDecomposition = true;
186 if(bNeedNewDecomposition)
188 // conditions of last local decomposition have changed, delete
189 const_cast< PolygonMarkerPrimitive2D* >(this)->setBuffered2DDecomposition(Primitive2DContainer());
192 if(getBuffered2DDecomposition().empty())
194 // remember last used InverseObjectToViewTransformation
195 PolygonMarkerPrimitive2D* pThat = const_cast< PolygonMarkerPrimitive2D* >(this);
196 pThat->maLastInverseObjectToViewTransformation = rViewInformation.getInverseObjectToViewTransformation();
199 // use parent implementation
200 BufferedDecompositionPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
203 // provide unique ID
204 ImplPrimitive2DIDBlock(PolygonMarkerPrimitive2D, PRIMITIVE2D_ID_POLYGONMARKERPRIMITIVE2D)
206 } // end of namespace primitive2d
207 } // end of namespace drawinglayer
209 namespace drawinglayer
211 double getRandomColorRange()
213 return comphelper::rng::uniform_real_distribution(0.0, nextafter(1.0, DBL_MAX));
216 namespace primitive2d
218 void PolygonStrokePrimitive2D::create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& /*rViewInformation*/) const
220 if(getB2DPolygon().count())
222 // #i102241# try to simplify before usage
223 const basegfx::B2DPolygon aB2DPolygon(basegfx::utils::simplifyCurveSegments(getB2DPolygon()));
224 basegfx::B2DPolyPolygon aHairLinePolyPolygon;
226 if(getStrokeAttribute().isDefault() || 0.0 == getStrokeAttribute().getFullDotDashLen())
228 // no line dashing, just copy
229 aHairLinePolyPolygon.append(aB2DPolygon);
231 else
233 // apply LineStyle
234 basegfx::utils::applyLineDashing(
235 aB2DPolygon, getStrokeAttribute().getDotDashArray(),
236 &aHairLinePolyPolygon, nullptr, getStrokeAttribute().getFullDotDashLen());
239 const sal_uInt32 nCount(aHairLinePolyPolygon.count());
241 if(!getLineAttribute().isDefault() && getLineAttribute().getWidth())
243 // create fat line data
244 const double fHalfLineWidth(getLineAttribute().getWidth() / 2.0);
245 const basegfx::B2DLineJoin aLineJoin(getLineAttribute().getLineJoin());
246 const css::drawing::LineCap aLineCap(getLineAttribute().getLineCap());
247 basegfx::B2DPolyPolygon aAreaPolyPolygon;
248 const double fMiterMinimumAngle(getLineAttribute().getMiterMinimumAngle());
250 for(sal_uInt32 a(0); a < nCount; a++)
252 // New version of createAreaGeometry; now creates bezier polygons
253 aAreaPolyPolygon.append(basegfx::utils::createAreaGeometry(
254 aHairLinePolyPolygon.getB2DPolygon(a),
255 fHalfLineWidth,
256 aLineJoin,
257 aLineCap,
258 basegfx::deg2rad(12.5) /* default fMaxAllowedAngle*/ ,
259 0.4 /* default fMaxPartOfEdge*/ ,
260 fMiterMinimumAngle));
263 // create primitive
264 for(sal_uInt32 b(0); b < aAreaPolyPolygon.count(); b++)
266 // put into single polyPolygon primitives to make clear that this is NOT meant
267 // to be painted as a single tools::PolyPolygon (XORed as fill rule). Alternatively, a
268 // melting process may be used here one day.
269 const basegfx::B2DPolyPolygon aNewPolyPolygon(aAreaPolyPolygon.getB2DPolygon(b));
270 const basegfx::BColor aColor(getLineAttribute().getColor());
271 rContainer.push_back(new PolyPolygonColorPrimitive2D(aNewPolyPolygon, aColor));
274 else
276 rContainer.push_back(
277 new PolyPolygonHairlinePrimitive2D(
278 aHairLinePolyPolygon,
279 getLineAttribute().getColor()));
284 PolygonStrokePrimitive2D::PolygonStrokePrimitive2D(
285 const basegfx::B2DPolygon& rPolygon,
286 const attribute::LineAttribute& rLineAttribute,
287 const attribute::StrokeAttribute& rStrokeAttribute)
288 : BufferedDecompositionPrimitive2D(),
289 maPolygon(rPolygon),
290 maLineAttribute(rLineAttribute),
291 maStrokeAttribute(rStrokeAttribute)
293 // simplify curve segments: moved here to not need to use it
294 // at VclPixelProcessor2D::tryDrawPolygonStrokePrimitive2DDirect
295 maPolygon = basegfx::utils::simplifyCurveSegments(maPolygon);
298 PolygonStrokePrimitive2D::PolygonStrokePrimitive2D(
299 const basegfx::B2DPolygon& rPolygon,
300 const attribute::LineAttribute& rLineAttribute)
301 : BufferedDecompositionPrimitive2D(),
302 maPolygon(rPolygon),
303 maLineAttribute(rLineAttribute),
304 maStrokeAttribute()
306 // simplify curve segments: moved here to not need to use it
307 // at VclPixelProcessor2D::tryDrawPolygonStrokePrimitive2DDirect
308 maPolygon = basegfx::utils::simplifyCurveSegments(maPolygon);
311 bool PolygonStrokePrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
313 if(BufferedDecompositionPrimitive2D::operator==(rPrimitive))
315 const PolygonStrokePrimitive2D& rCompare = static_cast<const PolygonStrokePrimitive2D&>(rPrimitive);
317 return (getB2DPolygon() == rCompare.getB2DPolygon()
318 && getLineAttribute() == rCompare.getLineAttribute()
319 && getStrokeAttribute() == rCompare.getStrokeAttribute());
322 return false;
325 basegfx::B2DRange PolygonStrokePrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
327 basegfx::B2DRange aRetval;
329 if(getLineAttribute().getWidth())
331 bool bUseDecomposition(false);
333 if(basegfx::B2DLineJoin::Miter == getLineAttribute().getLineJoin())
335 // if line is mitered, use parent call since mitered line
336 // geometry may use more space than the geometry grown by half line width
337 bUseDecomposition = true;
340 if(!bUseDecomposition && css::drawing::LineCap_SQUARE == getLineAttribute().getLineCap())
342 // when drawing::LineCap_SQUARE is used the below method to grow the polygon
343 // range by half line width will not work, so use decomposition. Interestingly,
344 // the grow method below works perfectly for LineCap_ROUND since the grow is in
345 // all directions and the rounded cap needs the same grow in all directions independent
346 // from its orientation. Unfortunately this is not the case for drawing::LineCap_SQUARE
347 bUseDecomposition = true;
350 if (bUseDecomposition)
352 // get correct range by using the decomposition fallback, reasons see above cases
354 // ofz#947 to optimize calculating the range, turn any slow dashes into a solid line
355 // when just calculating bounds
356 attribute::StrokeAttribute aOrigStrokeAttribute = maStrokeAttribute;
357 const_cast<PolygonStrokePrimitive2D*>(this)->maStrokeAttribute = attribute::StrokeAttribute();
358 aRetval = BufferedDecompositionPrimitive2D::getB2DRange(rViewInformation);
359 const_cast<PolygonStrokePrimitive2D*>(this)->maStrokeAttribute = aOrigStrokeAttribute;
361 else
363 // for all other B2DLINEJOIN_* get the range from the base geometry
364 // and expand by half the line width
365 aRetval = getB2DPolygon().getB2DRange();
366 aRetval.grow(getLineAttribute().getWidth() * 0.5);
369 else
371 // this is a hairline, thus the line width is view-dependent. Get range of polygon
372 // as base size
373 aRetval = getB2DPolygon().getB2DRange();
375 if(!aRetval.isEmpty())
377 // Calculate view-dependent hairline width
378 const basegfx::B2DVector aDiscreteSize(rViewInformation.getInverseObjectToViewTransformation() * basegfx::B2DVector(1.0, 0.0));
379 const double fDiscreteHalfLineWidth(aDiscreteSize.getLength() * 0.5);
381 if(basegfx::fTools::more(fDiscreteHalfLineWidth, 0.0))
383 aRetval.grow(fDiscreteHalfLineWidth);
388 return aRetval;
391 // provide unique ID
392 ImplPrimitive2DIDBlock(PolygonStrokePrimitive2D, PRIMITIVE2D_ID_POLYGONSTROKEPRIMITIVE2D)
394 } // end of namespace primitive2d
395 } // end of namespace drawinglayer
398 namespace drawinglayer
400 namespace primitive2d
402 void PolygonWavePrimitive2D::create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& /*rViewInformation*/) const
404 if(getB2DPolygon().count())
406 const bool bHasWidth(!basegfx::fTools::equalZero(getWaveWidth()));
407 const bool bHasHeight(!basegfx::fTools::equalZero(getWaveHeight()));
409 if(bHasWidth && bHasHeight)
411 // create waveline curve
412 const basegfx::B2DPolygon aWaveline(basegfx::utils::createWaveline(getB2DPolygon(), getWaveWidth(), getWaveHeight()));
413 rContainer.push_back(new PolygonStrokePrimitive2D(aWaveline, getLineAttribute(), getStrokeAttribute()));
415 else
417 // flat waveline, decompose to simple line primitive
418 rContainer.push_back(new PolygonStrokePrimitive2D(getB2DPolygon(), getLineAttribute(), getStrokeAttribute()));
423 PolygonWavePrimitive2D::PolygonWavePrimitive2D(
424 const basegfx::B2DPolygon& rPolygon,
425 const attribute::LineAttribute& rLineAttribute,
426 const attribute::StrokeAttribute& rStrokeAttribute,
427 double fWaveWidth,
428 double fWaveHeight)
429 : PolygonStrokePrimitive2D(rPolygon, rLineAttribute, rStrokeAttribute),
430 mfWaveWidth(fWaveWidth),
431 mfWaveHeight(fWaveHeight)
433 if(mfWaveWidth < 0.0)
435 mfWaveWidth = 0.0;
438 if(mfWaveHeight < 0.0)
440 mfWaveHeight = 0.0;
444 PolygonWavePrimitive2D::PolygonWavePrimitive2D(
445 const basegfx::B2DPolygon& rPolygon,
446 const attribute::LineAttribute& rLineAttribute,
447 double fWaveWidth,
448 double fWaveHeight)
449 : PolygonStrokePrimitive2D(rPolygon, rLineAttribute),
450 mfWaveWidth(fWaveWidth),
451 mfWaveHeight(fWaveHeight)
453 if(mfWaveWidth < 0.0)
455 mfWaveWidth = 0.0;
458 if(mfWaveHeight < 0.0)
460 mfWaveHeight = 0.0;
464 bool PolygonWavePrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
466 if(PolygonStrokePrimitive2D::operator==(rPrimitive))
468 const PolygonWavePrimitive2D& rCompare = static_cast<const PolygonWavePrimitive2D&>(rPrimitive);
470 return (getWaveWidth() == rCompare.getWaveWidth()
471 && getWaveHeight() == rCompare.getWaveHeight());
474 return false;
477 basegfx::B2DRange PolygonWavePrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
479 // get range of parent
480 basegfx::B2DRange aRetval(PolygonStrokePrimitive2D::getB2DRange(rViewInformation));
482 // if WaveHeight, grow by it
483 if(basegfx::fTools::more(getWaveHeight(), 0.0))
485 aRetval.grow(getWaveHeight());
488 // if line width, grow by it
489 if(basegfx::fTools::more(getLineAttribute().getWidth(), 0.0))
491 aRetval.grow(getLineAttribute().getWidth() * 0.5);
494 return aRetval;
497 // provide unique ID
498 ImplPrimitive2DIDBlock(PolygonWavePrimitive2D, PRIMITIVE2D_ID_POLYGONWAVEPRIMITIVE2D)
500 } // end of namespace primitive2d
501 } // end of namespace drawinglayer
504 namespace drawinglayer
506 namespace primitive2d
508 void PolygonStrokeArrowPrimitive2D::create2DDecomposition(Primitive2DContainer& rContainer, const geometry::ViewInformation2D& /*rViewInformation*/) const
510 // copy local polygon, it may be changed
511 basegfx::B2DPolygon aLocalPolygon(getB2DPolygon());
512 aLocalPolygon.removeDoublePoints();
513 basegfx::B2DPolyPolygon aArrowA;
514 basegfx::B2DPolyPolygon aArrowB;
516 if(!aLocalPolygon.isClosed() && aLocalPolygon.count() > 1)
518 // apply arrows
519 const double fPolyLength(basegfx::utils::getLength(aLocalPolygon));
520 double fStart(0.0);
521 double fEnd(0.0);
522 double fStartOverlap(0.0);
523 double fEndOverlap(0.0);
525 if(!getStart().isDefault() && getStart().isActive())
527 // create start arrow primitive and consume
528 aArrowA = basegfx::utils::createAreaGeometryForLineStartEnd(
529 aLocalPolygon, getStart().getB2DPolyPolygon(), true, getStart().getWidth(),
530 fPolyLength, getStart().isCentered() ? 0.5 : 0.0, &fStart);
532 // create some overlapping, compromise between straight and peaked markers
533 // for marker width 0.3cm and marker line width 0.02cm
534 fStartOverlap = getStart().getWidth() / 15.0;
537 if(!getEnd().isDefault() && getEnd().isActive())
539 // create end arrow primitive and consume
540 aArrowB = basegfx::utils::createAreaGeometryForLineStartEnd(
541 aLocalPolygon, getEnd().getB2DPolyPolygon(), false, getEnd().getWidth(),
542 fPolyLength, getEnd().isCentered() ? 0.5 : 0.0, &fEnd);
544 // create some overlapping
545 fEndOverlap = getEnd().getWidth() / 15.0;
548 if(0.0 != fStart || 0.0 != fEnd)
550 // build new poly, consume something from old poly
551 aLocalPolygon = basegfx::utils::getSnippetAbsolute(aLocalPolygon, fStart-fStartOverlap, fPolyLength - fEnd + fEndOverlap, fPolyLength);
555 // add shaft
556 rContainer.push_back(new
557 PolygonStrokePrimitive2D(
558 aLocalPolygon, getLineAttribute(), getStrokeAttribute()));
560 if(aArrowA.count())
562 rContainer.push_back(
563 new PolyPolygonColorPrimitive2D(
564 aArrowA, getLineAttribute().getColor()));
567 if(aArrowB.count())
569 rContainer.push_back(
570 new PolyPolygonColorPrimitive2D(
571 aArrowB, getLineAttribute().getColor()));
575 PolygonStrokeArrowPrimitive2D::PolygonStrokeArrowPrimitive2D(
576 const basegfx::B2DPolygon& rPolygon,
577 const attribute::LineAttribute& rLineAttribute,
578 const attribute::StrokeAttribute& rStrokeAttribute,
579 const attribute::LineStartEndAttribute& rStart,
580 const attribute::LineStartEndAttribute& rEnd)
581 : PolygonStrokePrimitive2D(rPolygon, rLineAttribute, rStrokeAttribute),
582 maStart(rStart),
583 maEnd(rEnd)
587 PolygonStrokeArrowPrimitive2D::PolygonStrokeArrowPrimitive2D(
588 const basegfx::B2DPolygon& rPolygon,
589 const attribute::LineAttribute& rLineAttribute,
590 const attribute::LineStartEndAttribute& rStart,
591 const attribute::LineStartEndAttribute& rEnd)
592 : PolygonStrokePrimitive2D(rPolygon, rLineAttribute),
593 maStart(rStart),
594 maEnd(rEnd)
598 bool PolygonStrokeArrowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
600 if(PolygonStrokePrimitive2D::operator==(rPrimitive))
602 const PolygonStrokeArrowPrimitive2D& rCompare = static_cast<const PolygonStrokeArrowPrimitive2D&>(rPrimitive);
604 return (getStart() == rCompare.getStart()
605 && getEnd() == rCompare.getEnd());
608 return false;
611 basegfx::B2DRange PolygonStrokeArrowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
613 if(getStart().isActive() || getEnd().isActive())
615 // use decomposition when line start/end is used
616 return BufferedDecompositionPrimitive2D::getB2DRange(rViewInformation);
618 else
620 // get range from parent
621 return PolygonStrokePrimitive2D::getB2DRange(rViewInformation);
625 // provide unique ID
626 ImplPrimitive2DIDBlock(PolygonStrokeArrowPrimitive2D, PRIMITIVE2D_ID_POLYGONSTROKEARROWPRIMITIVE2D)
628 } // end of namespace primitive2d
629 } // end of namespace drawinglayer
631 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */