/* * Copyright 2006 The Android Open Source Project * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file.
*/
/** * Path.bounds is defined to be the bounds of all the control points. * If we called bounds.join(r) we would skip r if r was empty, which breaks * our promise. Hence we have a custom joiner that doesn't look at emptiness
*/ staticvoid joinNoEmptyChecks(SkRect* dst, const SkRect& src) {
dst->fLeft = std::min(dst->fLeft, src.fLeft);
dst->fTop = std::min(dst->fTop, src.fTop);
dst->fRight = std::max(dst->fRight, src.fRight);
dst->fBottom = std::max(dst->fBottom, src.fBottom);
}
/* This class's constructor/destructor bracket a path editing operation. It is used when we know the bounds of the amount we are going to add to the path (usually a new contour, but not required).
It captures some state about the path up front (i.e. if it already has a cached bounds), and then if it can, it updates the cache bounds explicitly, avoiding the need to revisit all of the points in getBounds().
It also notes if the path was originally degenerate, and if so, sets isConvex to true. Thus it can only be used if the contour being added is convex.
*/ class SkAutoPathBoundsUpdate { public:
SkAutoPathBoundsUpdate(SkPath* path, const SkRect& r) : fPath(path), fRect(r) { // Cannot use fRect for our bounds unless we know it is sorted
fRect.sort(); // Mark the path's bounds as dirty if (1) they are, or (2) the path // is non-finite, and therefore its bounds are not meaningful
fHasValidBounds = path->hasComputedBounds() && path->isFinite();
fEmpty = path->isEmpty(); if (fHasValidBounds && !fEmpty) {
joinNoEmptyChecks(&fRect, fPath->getBounds());
}
fDegenerate = is_degenerate(*path);
}
/* Stores the verbs and points as they are given to us, with exceptions: - we only record "Close" if it was immediately preceeded by Move | Line | Quad | Cubic - we insert a Move(0,0) if Line | Quad | Cubic is our first command
The iterator does more cleanup, especially if forceClose == true 1. If we encounter degenerate segments, remove them 2. if we encounter Close, return a cons'd up Line() first (if the curr-pt != start-pt) 3. if we encounter Move without a preceeding Close, and forceClose is true, goto #2 4. if we encounter Line | Quad | Cubic after Close, cons up a Move
*/
// flag to require a moveTo if we begin with something else, like lineTo etc. // This will also be the value of lastMoveToIndex for a single contour // ending with close, so countVerbs needs to be checked against 0. #define INITIAL_LASTMOVETOINDEX_VALUE ~0
void SkPath::resetFields() { //fPathRef is assumed to have been emptied by the caller.
fLastMoveToIndex = INITIAL_LASTMOVETOINDEX_VALUE;
fFillType = SkToU8(SkPathFillType::kWinding);
this->setConvexity(SkPathConvexity::kUnknown);
this->setFirstDirection(SkPathFirstDirection::kUnknown);
}
void SkPath::copyFields(const SkPath& that) { //fPathRef is assumed to have been set by the caller.
fLastMoveToIndex = that.fLastMoveToIndex;
fFillType = that.fFillType;
fIsVolatile = that.fIsVolatile;
// Non-atomic assignment of atomic values.
this->setConvexity(that.getConvexityOrUnknown());
this->setFirstDirection(that.getFirstDirection());
}
booloperator==(const SkPath& a, const SkPath& b) { // note: don't need to look at isConvex or bounds, since just comparing the // raw data is sufficient. return &a == &b ||
(a.fFillType == b.fFillType && *a.fPathRef == *b.fPathRef);
}
for (auto [verb, pts, weight] : SkPathPriv::Iterate(*this)) { if (verb == SkPathVerb::kClose || (segmentCount > 0 && verb == SkPathVerb::kMove)) { // Closing the current contour; but since convexity is a precondition, it's the only // contour that matters.
SkASSERT(moveCnt);
segmentCount++; break;
} elseif (verb == SkPathVerb::kMove) { // A move at the start of the contour (or multiple leading moves, in which case we // keep the last one before a non-move verb).
SkASSERT(!segmentCount);
SkDEBUGCODE(++moveCnt);
firstPt = prevPt = pts[0];
} else { int pointCount = SkPathPriv::PtsInVerb((unsigned) verb);
SkASSERT(pointCount > 0);
if (!SkPathPriv::AllPointsEq(pts, pointCount + 1)) {
SkASSERT(moveCnt); int nextPt = pointCount;
segmentCount++;
if (prevPt == pts[nextPt]) { // A pre-condition to getting here is that the path is convex, so if a // verb's start and end points are the same, it means it's the only // verb in the contour (and the only contour). While it's possible for // such a single verb to be a convex curve, we do not have any non-zero // length edges to conservatively test against without splitting or // evaluating the curve. For simplicity, just reject the rectangle. returnfalse;
} elseif (SkPathVerb::kConic == verb) {
SkConic orig;
orig.set(pts, *weight);
SkPoint quadPts[5]; int count = orig.chopIntoQuadsPOW2(quadPts, 1);
SkASSERT_RELEASE(2 == count);
void SkPath::validateRef() const { // This will SkASSERT if not valid.
fPathRef->validate();
} #endif /* Determines if path is a rect by keeping track of changes in direction and looking for a loop either clockwise or counterclockwise.
The direction is computed such that: 0: vertical up 1: horizontal left 2: vertical down 3: horizontal right
A rectangle cycles up/right/down/left or up/left/down/right.
The test fails if: The path is closed, and followed by a line. A second move creates a new endpoint. A diagonal line is parsed. There's more than four changes of direction. There's a discontinuity on the line (e.g., a move in the middle) The line reverses direction. The path contains a quadratic or cubic. The path contains fewer than four points. *The rectangle doesn't complete a cycle. *The final point isn't equal to the first point.
*These last two conditions we relax if we have a 3-edge path that would form a rectangle if it were closed (as we do when we fill a path)
It's OK if the path has: Several colinear line segments composing a rectangle side. Single points on the rectangle side.
The direction takes advantage of the corners found since opposite sides must travel in opposite directions.
FIXME: Allow colinear quads and cubics to be treated like lines. FIXME: If the API passes fill-only, return true if the filled stroke is a rectangle, though the caller failed to close the path.
directions values: 0x1 is set if the segment is horizontal 0x2 is set if the segment is moving to the right or down thus: two directions are opposites iff (dirA ^ dirB) == 0x2 two directions are perpendicular iff (dirA ^ dirB) == 0x1
int count = fPathRef->countPoints(); if (count == 0) {
this->moveTo(x, y);
} else {
SkPathRef::Editor ed(&fPathRef);
ed.atPoint(count-1)->set(x, y);
}
}
// This is the public-facing non-const setConvexity(). void SkPath::setConvexity(SkPathConvexity c) {
fConvexity.store((uint8_t)c, std::memory_order_relaxed);
}
// Const hooks for working with fConvexity and fFirstDirection from const methods. void SkPath::setConvexity(SkPathConvexity c) const {
fConvexity.store((uint8_t)c, std::memory_order_relaxed);
} void SkPath::setFirstDirection(SkPathFirstDirection d) const {
fFirstDirection.store((uint8_t)d, std::memory_order_relaxed);
}
SkPathFirstDirection SkPath::getFirstDirection() const { return (SkPathFirstDirection)fFirstDirection.load(std::memory_order_relaxed);
}
bool SkPath::isConvexityAccurate() const {
SkPathConvexity convexity = this->getConvexityOrUnknown(); if (convexity != SkPathConvexity::kUnknown) { auto conv = this->computeConvexity(); if (conv != convexity) {
SkASSERT(false); returnfalse;
}
} returntrue;
}
SkPathConvexity SkPath::getConvexity() const { // Enable once we fix all the bugs // SkDEBUGCODE(this->isConvexityAccurate());
SkPathConvexity convexity = this->getConvexityOrUnknown(); if (convexity == SkPathConvexity::kUnknown) {
convexity = this->computeConvexity();
}
SkASSERT(convexity != SkPathConvexity::kUnknown); return convexity;
}
////////////////////////////////////////////////////////////////////////////// // Construction methods
#ifdef SK_DEBUG // enable this as needed for testing, but it slows down some chrome tests so much // that they don't complete, so we don't enable it by default // e.g. TEST(IdentifiabilityPaintOpDigestTest, MassiveOpSkipped) if (this->countVerbs() < 16) {
SkASSERT(fPathRef->dataMatchesVerbs());
} #endif
return *this;
}
void SkPath::incReserve(int extraPtCount, int extraVerbCount, int extraConicCount) {
SkDEBUGCODE(this->validate();) if (extraPtCount > 0) { // For compat with when this function only took a single argument, use // extraPtCount if extraVerbCount is 0 (default value).
SkPathRef::Editor(&fPathRef, extraVerbCount == 0 ? extraPtCount : extraVerbCount, extraPtCount, extraConicCount);
}
SkDEBUGCODE(this->validate();)
}
int count = fPathRef->countVerbs(); if (count > 0) { switch (fPathRef->atVerb(count - 1)) { case kLine_Verb: case kQuad_Verb: case kConic_Verb: case kCubic_Verb: case kMove_Verb: {
SkPathRef::Editor ed(&fPathRef);
ed.growForVerb(kClose_Verb); break;
} case kClose_Verb: // don't add a close if it's the first verb or a repeat break; default:
SkDEBUGFAIL("unexpected verb"); break;
}
}
// signal that we need a moveTo to follow us (unless we're done) #if 0 if (fLastMoveToIndex >= 0) {
fLastMoveToIndex = ~fLastMoveToIndex;
} #else
fLastMoveToIndex ^= ~fLastMoveToIndex >> (8 * sizeof(fLastMoveToIndex) - 1); #endif return *this;
}
staticbool arc_is_lone_point(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle,
SkPoint* pt) { if (0 == sweepAngle && (0 == startAngle || SkIntToScalar(360) == startAngle)) { // Chrome uses this path to move into and out of ovals. If not // treated as a special case the moves can distort the oval's // bounding box (and break the circle special case).
pt->set(oval.fRight, oval.centerY()); returntrue;
} elseif (0 == oval.width() && 0 == oval.height()) { // Chrome will sometimes create 0 radius round rects. Having degenerate // quad segments in the path prevents the path from being recognized as // a rect. // TODO: optimizing the case where only one of width or height is zero // should also be considered. This case, however, doesn't seem to be // as common as the single point case.
pt->set(oval.fRight, oval.fTop); returntrue;
} returnfalse;
}
// Return the unit vectors pointing at the start/stop points for the given start/sweep angles // staticvoid angles_to_unit_vectors(SkScalar startAngle, SkScalar sweepAngle,
SkVector* startV, SkVector* stopV, SkRotationDirection* dir) {
SkScalar startRad = SkDegreesToRadians(startAngle),
stopRad = SkDegreesToRadians(startAngle + sweepAngle);
/* If the sweep angle is nearly (but less than) 360, then due to precision loss in radians-conversion and/or sin/cos, we may end up with coincident vectors, which will fool SkBuildQuadArc into doing nothing (bad) instead of drawing a nearly complete circle (good). e.g. canvas.drawArc(0, 359.99, ...) -vs- canvas.drawArc(0, 359.9, ...) We try to detect this edge case, and tweak the stop vector
*/ if (*startV == *stopV) {
SkScalar sw = SkScalarAbs(sweepAngle); if (sw < SkIntToScalar(360) && sw > SkIntToScalar(359)) { // make a guess at a tiny angle (in radians) to tweak by
SkScalar deltaRad = SkScalarCopySign(SK_Scalar1/512, sweepAngle); // not sure how much will be enough, so we use a loop do {
stopRad -= deltaRad;
stopV->fY = SkScalarSinSnapToZero(stopRad);
stopV->fX = SkScalarCosSnapToZero(stopRad);
} while (*startV == *stopV);
}
}
*dir = sweepAngle > 0 ? kCW_SkRotationDirection : kCCW_SkRotationDirection;
}
/** * If this returns 0, then the caller should just line-to the singlePt, else it should * ignore singlePt and append the specified number of conics.
*/ staticint build_arc_conics(const SkRect& oval, const SkVector& start, const SkVector& stop,
SkRotationDirection dir, SkConic conics[SkConic::kMaxConicsForArc],
SkPoint* singlePt) {
SkMatrix matrix;
// we start with a conic on odd indices when moving CW vs. even indices when moving CCW constbool startsWithConic = ((startIndex & 1) == (dir == SkPathDirection::kCW)); const SkScalar weight = SK_ScalarRoot2Over2;
/* If addOval() is called after previous moveTo(), this path is still marked as an oval. This is used to fit into WebKit's calling sequences. We can't simply check isEmpty() in this case, as additional moveTo() would mark the path non empty.
*/ bool isOval = hasOnlyMoveTos(); if (isOval) {
this->setFirstDirection((SkPathFirstDirection)dir);
} else {
this->setFirstDirection(SkPathFirstDirection::kUnknown);
}
// Adds a move-to to 'pt' if forceMoveTo is true. Otherwise a lineTo unless we're sufficiently // close to 'pt' currently. This prevents spurious lineTos when adding a series of contiguous // arcs from the same oval. auto addPt = [&forceMoveTo, &isArc, this](const SkPoint& pt) {
SkPoint lastPt; if (forceMoveTo) {
this->moveTo(pt);
} elseif (!this->getLastPt(&lastPt) ||
!SkScalarNearlyEqual(lastPt.fX, pt.fX) ||
!SkScalarNearlyEqual(lastPt.fY, pt.fY)) {
this->lineTo(pt);
isArc = false;
}
};
// At this point, we know that the arc is not a lone point, but startV == stopV // indicates that the sweepAngle is too small such that angles_to_unit_vectors // cannot handle it. if (startV == stopV) {
SkScalar endAngle = SkDegreesToRadians(startAngle + sweepAngle);
SkScalar radiusX = oval.width() / 2;
SkScalar radiusY = oval.height() / 2; // We do not use SkScalar[Sin|Cos]SnapToZero here. When sin(startAngle) is 0 and sweepAngle // is very small and radius is huge, the expected behavior here is to draw a line. But // calling SkScalarSinSnapToZero will make sin(endAngle) be 0 which will then draw a dot.
singlePt.set(oval.centerX() + radiusX * SkScalarCos(endAngle),
oval.centerY() + radiusY * SkScalarSin(endAngle));
addPt(singlePt); return *this;
}
SkConic conics[SkConic::kMaxConicsForArc]; int count = build_arc_conics(oval, startV, stopV, dir, conics, &singlePt); if (count) { // Conics take two points. Add one to the verb in case there is a moveto.
this->incReserve(count * 2 + 1, count + 1, count); const SkPoint& pt = conics[0].fPts[0];
addPt(pt); for (int i = 0; i < count; ++i) {
this->conicTo(conics[i].fPts[1], conics[i].fPts[2], conics[i].fW);
} if (isArc) {
SkPathRef::Editor ed(&fPathRef);
ed.setIsArc(SkArc::Make(oval, startAngle, sweepAngle, SkArc::Type::kArc));
}
} else {
addPt(singlePt);
} return *this;
}
// This converts the SVG arc to conics. // Partly adapted from Niko's code in kdelibs/kdecore/svgicons. // Then transcribed from webkit/chrome's SVGPathNormalizer::decomposeArcToCubic() // See also SVG implementation notes: // http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter // Note that arcSweep bool value is flipped from the original implementation.
SkPath& SkPath::arcTo(SkScalar rx, SkScalar ry, SkScalar angle, SkPath::ArcSize arcLarge,
SkPathDirection arcSweep, SkScalar x, SkScalar y) {
this->injectMoveToIfNeeded();
SkPoint srcPts[2];
this->getLastPt(&srcPts[0]); // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") // joining the endpoints. // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters if (!rx || !ry) { return this->lineTo(x, y);
} // If the current point and target point for the arc are identical, it should be treated as a // zero length path. This ensures continuity in animations.
srcPts[1].set(x, y); if (srcPts[0] == srcPts[1]) { return this->lineTo(x, y);
}
rx = SkScalarAbs(rx);
ry = SkScalarAbs(ry);
SkVector midPointDistance = srcPts[0] - srcPts[1];
midPointDistance *= 0.5f;
// Check if the radii are big enough to draw the arc, scale radii if not. // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
SkScalar radiiScale = squareX / squareRx + squareY / squareRy; if (radiiScale > 1) {
radiiScale = SkScalarSqrt(radiiScale);
rx *= radiiScale;
ry *= radiiScale;
}
SkScalar d = delta.fX * delta.fX + delta.fY * delta.fY;
SkScalar scaleFactorSquared = std::max(1 / d - 0.25f, 0.f);
SkScalar scaleFactor = SkScalarSqrt(scaleFactorSquared); if ((arcSweep == SkPathDirection::kCCW) != SkToBool(arcLarge)) { // flipped from the original implementation
scaleFactor = -scaleFactor;
}
delta.scale(scaleFactor);
SkPoint centerPoint = unitPts[0] + unitPts[1];
centerPoint *= 0.5f;
centerPoint.offset(-delta.fY, delta.fX);
unitPts[0] -= centerPoint;
unitPts[1] -= centerPoint;
SkScalar theta1 = SkScalarATan2(unitPts[0].fY, unitPts[0].fX);
SkScalar theta2 = SkScalarATan2(unitPts[1].fY, unitPts[1].fX);
SkScalar thetaArc = theta2 - theta1; if (thetaArc < 0 && (arcSweep == SkPathDirection::kCW)) { // arcSweep flipped from the original implementation
thetaArc += SK_ScalarPI * 2;
} elseif (thetaArc > 0 && (arcSweep != SkPathDirection::kCW)) { // arcSweep flipped from the original implementation
thetaArc -= SK_ScalarPI * 2;
}
// Very tiny angles cause our subsequent math to go wonky (skbug.com/9272) // so we do a quick check here. The precise tolerance amount is just made up. // PI/million happens to fix the bug in 9272, but a larger value is probably // ok too. if (SkScalarAbs(thetaArc) < (SK_ScalarPI / (1000 * 1000))) { return this->lineTo(x, y);
}
// the arc may be slightly bigger than 1/4 circle, so allow up to 1/3rd int segments = SkScalarCeilToInt(SkScalarAbs(thetaArc / (2 * SK_ScalarPI / 3)));
SkScalar thetaWidth = thetaArc / segments;
SkScalar t = SkScalarTan(0.5f * thetaWidth); if (!SkIsFinite(t)) { return *this;
}
SkScalar startTheta = theta1;
SkScalar w = SkScalarSqrt(SK_ScalarHalf + SkScalarCos(thetaWidth) * SK_ScalarHalf); auto scalar_is_integer = [](SkScalar scalar) -> bool { return scalar == SkScalarFloorToScalar(scalar);
}; bool expectIntegers = SkScalarNearlyZero(SK_ScalarPI/2 - SkScalarAbs(thetaWidth)) &&
scalar_is_integer(rx) && scalar_is_integer(ry) &&
scalar_is_integer(x) && scalar_is_integer(y);
for (int i = 0; i < segments; ++i) {
SkScalar endTheta = startTheta + thetaWidth,
sinEndTheta = SkScalarSinSnapToZero(endTheta),
cosEndTheta = SkScalarCosSnapToZero(endTheta);
unitPts[1].set(cosEndTheta, sinEndTheta);
unitPts[1] += centerPoint;
unitPts[0] = unitPts[1];
unitPts[0].offset(t * sinEndTheta, -t * cosEndTheta);
SkPoint mapped[2];
pointTransform.mapPoints(mapped, unitPts, (int) std::size(unitPts)); /* Computing the arc width introduces rounding errors that cause arcs to start outside their marks. A round rect may lose convexity as a result. If the input values are on integers, place the conic on integers as well.
*/ if (expectIntegers) { for (SkPoint& point : mapped) {
point.fX = SkScalarRoundToScalar(point.fX);
point.fY = SkScalarRoundToScalar(point.fY);
}
}
this->conicTo(mapped[0], mapped[1], w);
startTheta = endTheta;
}
// The final point should match the input point (by definition); replace it to // ensure that rounding errors in the above math don't cause any problems.
this->setLastPt(x, y); return *this;
}
if (sweepAngle >= kFullCircleAngle || sweepAngle <= -kFullCircleAngle) { // We can treat the arc as an oval if it begins at one of our legal starting positions. // See SkPath::addOval() docs.
SkScalar startOver90 = startAngle / 90.f;
SkScalar startOver90I = SkScalarRoundToScalar(startOver90);
SkScalar error = startOver90 - startOver90I; if (SkScalarNearlyEqual(error, 0)) { // Index 1 is at startAngle == 0.
SkScalar startIndex = std::fmod(startOver90I + 1.f, 4.f);
startIndex = startIndex < 0 ? startIndex + 4.f : startIndex; return this->addOval(oval, sweepAngle > 0 ? SkPathDirection::kCW : SkPathDirection::kCCW,
(unsigned) startIndex);
}
} return this->arcTo(oval, startAngle, sweepAngle, true);
}
/* Need to handle the case when the angle is sharp, and our computed end-points for the arc go behind pt1 and/or p2...
*/
SkPath& SkPath::arcTo(SkScalar x1, SkScalar y1, SkScalar x2, SkScalar y2, SkScalar radius) {
this->injectMoveToIfNeeded();
if (radius == 0) { return this->lineTo(x1, y1);
}
// need to know our prev pt so we can construct tangent vectors
SkPoint start;
this->getLastPt(&start);
// need double precision for these calcs.
skvx::double2 befored = normalize(skvx::double2{x1 - start.fX, y1 - start.fY});
skvx::double2 afterd = normalize(skvx::double2{x2 - x1, y2 - y1}); double cosh = dot(befored, afterd); double sinh = cross(befored, afterd);
// If the previous point equals the first point, befored will be denormalized. // If the two points equal, afterd will be denormalized. // If the second point equals the first point, sinh will be zero. // In all these cases, we cannot construct an arc, so we construct a line to the first point. if (!isfinite(befored) || !isfinite(afterd) || SkScalarNearlyZero(SkDoubleToScalar(sinh))) { return this->lineTo(x1, y1);
}
// safe to convert back to floats now
SkScalar dist = SkScalarAbs(SkDoubleToScalar(radius * (1 - cosh) / sinh));
SkScalar xx = x1 - dist * befored[0];
SkScalar yy = y1 - dist * befored[1];
while (verbs > verbsBegin) {
uint8_t v = *--verbs;
pts -= SkPathPriv::PtsInVerb(v); switch (v) { case kMove_Verb: // if the path has multiple contours, stop after reversing the last return *this; case kLine_Verb:
this->lineTo(pts[0]); break; case kQuad_Verb:
this->quadTo(pts[1], pts[0]); break; case kConic_Verb:
this->conicTo(pts[1], pts[0], *--conicWeights); break; case kCubic_Verb:
this->cubicTo(pts[2], pts[1], pts[0]); break; case kClose_Verb: break; default:
SkDEBUGFAIL("bad verb"); break;
}
} return *this;
}
SkPath& SkPath::reverseAddPath(const SkPath& srcPath) { // Detect if we're trying to add ourself const SkPath* src = &srcPath;
SkTLazy<SkPath> tmp; if (this == src) {
src = tmp.set(srcPath);
}
// Due to finite/fragile float numerics, we can't assume that a convex path remains // convex after a transformation, so mark it as unknown here. // However, some transformations are thought to be safe: // axis-aligned values under scale/translate. // if (convexity == SkPathConvexity::kConvex &&
(!matrix.isScaleTranslate() || !SkPathPriv::IsAxisAligned(*this))) { // Not safe to still assume we're convex...
convexity = SkPathConvexity::kUnknown;
}
dst->setConvexity(convexity);
if (kMove_Verb == *verbs) {
verbs += 1; // skip the initial moveto
}
while (verbs < stop) { // verbs points one beyond the current verb, decrement first. unsigned v = *verbs++; if (kMove_Verb == v) { break;
} if (kClose_Verb == v) { returntrue;
}
} returnfalse;
}
SkPath::Verb SkPath::Iter::autoClose(SkPoint pts[2]) {
SkASSERT(pts); if (fLastPt != fMoveTo) { // A special case: if both points are NaN, SkPoint::operation== returns // false, but the iterator expects that they are treated as the same. // (consider SkPoint is a 2-dimension float point). if (SkIsNaN(fLastPt.fX) || SkIsNaN(fLastPt.fY) ||
SkIsNaN(fMoveTo.fX) || SkIsNaN(fMoveTo.fY)) { return kClose_Verb;
}
if (fVerbs == fVerbStop) { // Close the curve if requested and if there is some curve to close if (fNeedClose) { if (kLine_Verb == this->autoClose(ptsParam)) { return kLine_Verb;
}
fNeedClose = false; return kClose_Verb;
} return kDone_Verb;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.