/**************************************************************************\ * * Copyright (c) 1999 - 2000 Microsoft Corporation * * Module Name: * * PathWidener.cpp * * Abstract: * * Implementation of the GpPathWidener class * * Revision History: * * 11/23/99 ikkof * Created it * \**************************************************************************/ #include "precomp.hpp" // 4*(REALSQRT(2.0) - 1)/3 #define U_CIR ((REAL)(0.552284749)) GpStatus GetMajorAndMinorAxis( REAL* majorR, REAL* minorR, const GpMatrix* matrix ); /**************************************************************************\ * * Function Description: * * This calculates the major and minor radius of an oval * when the unit cricle is transformed by the given matrix. * For further details, see ikkof's notes on Pen Transform. * * Arguments: * * [OUT] majorR - the major radius. * [OUT] minorR - the minor radius. * [IN] matrix - the matrix to transform the unit circle. * * Return Value: * * Status * * 01/28/00 ikkof * Created it * \**************************************************************************/ GpStatus GetMajorAndMinorAxis(REAL* majorR, REAL* minorR, const GpMatrix* matrix) { if(matrix == NULL) { // Regard this as an identity matrix. *majorR = 1; *minorR = 1; return Ok; } REAL m11 = matrix->GetM11(); REAL m12 = matrix->GetM12(); REAL m21 = matrix->GetM21(); REAL m22 = matrix->GetM22(); REAL d1 = ((m11*m11 + m12*m12) - (m21*m21 + m22*m22))/2; REAL d2 = m11*m21 + m12*m22; REAL D = d1*d1 + d2*d2; if(D > 0) D = REALSQRT(D); REAL r0 = (m11*m11 + m12*m12 + m21*m21 + m22*m22)/2; REAL r1 = REALSQRT(r0 + D); REAL r2 = REALSQRT(r0 - D); // They should be positive numbers. Prevent the floating // point underflow. if(r1 <= CPLX_EPSILON) r1 = CPLX_EPSILON; if(r2 <= CPLX_EPSILON) r2 = CPLX_EPSILON; *majorR = r1; *minorR = r2; return Ok; } /**************************************************************************\ * * Function Description: * * Constructor for the GpPathWidener. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ GpPathWidener::GpPathWidener( GpPath *path, const DpPen* pen, GpMatrix *matrix, BOOL doubleCaps ) { // Must call flatten with an appropriate flatten tolerance before widening. ASSERT(!path->HasCurve()); Path = path; Pen = pen; DoubleCaps = doubleCaps && Pen->CompoundCount == 0; StrokeWidth = Pen->Width; // Set to identity. XForm.Reset(); if(matrix) { XForm = *matrix; // Otherwise XForm remains Identity. } // Apply the pen transform too. if(!(Pen->Xform.IsIdentity())) { XForm.Prepend(Pen->Xform); } // Compute the unit scale. REAL majorR, minorR; GetMajorAndMinorAxis(&majorR, &minorR, &XForm); REAL unitScale = min(majorR, minorR); // Set minimum width to 1.0 (plus a bit for possible precision errors), // so that narrow width pens don't end up leaving gaps in the line. // This is the minimum allowable width in device pixels REAL minDeviceWidth = 1.000001f; if(DoubleCaps) { // Double Caps require wider minimum pens. minDeviceWidth *= 2.0f; // Dashes smaller than a pixel are dropping out entirely in inset // pen because of the rasterizer pixel level clipping that is taking // place. We increase the minimum width of dashed lines making them // roughly 4.0f. This also helps address the weird moire aliasing // effects with the really small dash-dot round lines. if(Pen->DashStyle != DashStyleSolid) { minDeviceWidth *= 2.0f; } } REAL minWorldWidth = minDeviceWidth/unitScale; // StrokeWidth is in World coordinates - compare against the minimum // world stroke width. if(StrokeWidth < minWorldWidth) { StrokeWidth = minWorldWidth; } SetValid(TRUE); } /**************************************************************************\ * * Function Description: * * Computes the Normal vectors for a single subpath. It reuses the * normalArray input DynArray's memory allocation for the values. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID GpPathWidener::ComputeSubpathNormals( DynArray *normalArray, const INT count, const BOOL isClosed, const GpPointF *points ) { // parameter validation. ASSERT(points != NULL); ASSERT(normalArray != NULL); // Set the count to zero, but don't free the memory. // Then update the count to store enough normals for this subpath, and // allocate the memory if required. normalArray->Reset(FALSE); GpVector2D *normals = normalArray->AddMultiple(count); // Allocation failure or no points. if(normals == NULL) { return; } // For each point in the subpath // Compute the normal at this point. INT ptIndex; for(ptIndex = 0; ptIndex < count; ptIndex++) { // Work out the previous point relative to ptIndex INT ptIndexPrev = ptIndex-1; // If this is the first point, we need to decide how to process // previous based on the closed status of the path. If it's closed, // wrap, otherwise the normal at the first point is meaningless. if(ptIndexPrev < 0) { if(isClosed) { ptIndexPrev = count-1; } else { ptIndexPrev = 0; } } // Compute the normal at this point by looking at this point and // the previous one. normals[ptIndex] = points[ptIndex]- points[ptIndexPrev]; ASSERT( ptIndexPrev==ptIndex || REALABS(normals[ptIndex].X) > REAL_EPSILON || REALABS(normals[ptIndex].Y) > REAL_EPSILON ); normals[ptIndex].Normalize(); REAL tmp = normals[ptIndex].X; normals[ptIndex].X = normals[ptIndex].Y; normals[ptIndex].Y = -tmp; } // Apply the pen transform if there is one. if(!Pen->Xform.IsIdentity()) { Pen->Xform.VectorTransform(normals, count); } } /**************************************************************************\ * * Function Description: * * Computes the List of non-degenerate points for a single subpath. It reuses * the filteredPoints input DynArray's memory allocation for the values. * * History: * * 10/23/2000 asecchia * Created. * \**************************************************************************/ GpStatus GpPathWidener::ComputeNonDegeneratePoints( DynArray *filteredPoints, const GpPath::SubpathInfo &subpath, const GpPointF *points ) { // parameter validation. ASSERT(points != NULL); ASSERT(filteredPoints != NULL); // Set the count to zero, but don't free the memory. // Then update the count to store enough normals for this subpath, and // allocate the memory if required. filteredPoints->Reset(FALSE); // nothing to do. if(subpath.Count == 0) { return Ok; } // For each point in the subpath decide if we add the point. const GpPointF *lastPoint = points + subpath.StartIndex; if(filteredPoints->Add(points[subpath.StartIndex]) != Ok) { return OutOfMemory; } INT ptIndex; for(ptIndex = subpath.StartIndex+1; ptIndex < subpath.StartIndex+subpath.Count; ptIndex++) { // !!! we should be using the flattening tolerance for this // instead of REAL_EPSILON - it would be much more efficient. if( REALABS(lastPoint->X-points[ptIndex].X) > REAL_EPSILON || REALABS(lastPoint->Y-points[ptIndex].Y) > REAL_EPSILON ) { if(filteredPoints->Add(points[ptIndex]) != Ok) { return OutOfMemory; } } lastPoint = points + ptIndex; } if(filteredPoints->GetCount() <= 1) { // If everything degenerated, erase the first point too. filteredPoints->Reset(FALSE); } return Ok; } /**************************************************************************\ * * Description: * * Function Pointer to a Join function. * * History: * * 10/22/2000 asecchia * Created. * \**************************************************************************/ typedef VOID (*JoinProc)( const GpVector2D &, const GpVector2D &, const GpPointF &, const REAL , GpPath * ); /**************************************************************************\ * * Function Description: * * Performs an inside join on the input normals and point. The resulting * join point - if any - is added to the path. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID InsideJoin( const GpVector2D &normalCurr, const GpVector2D &normalNext, const GpPointF &ptStart, const GpPointF &ptCurr, const GpPointF &ptNext, GpPath *path ) { // Inside join. REAL t1, t2; // parametric line lengths for the intersection. GpPointF ptJoin; if( IntersectLines( ptStart + normalCurr, ptCurr + normalCurr, ptStart + normalNext, ptNext + normalNext, &t1, &t2, &ptJoin )) { if( (t1 > (REAL_EPSILON)) && (t2 > (REAL_EPSILON)) && ((t1-1.0f) < (-REAL_EPSILON)) && ((t2-1.0f) < (-REAL_EPSILON)) ) { path->AddWidenPoint(ptJoin); } else { // intersection outside of legal range of the two edge pieces. // Add the icky backward loopy thing. If the caller really needs // no loops it needs to call the PathSelfIntersectRemover. path->AddWidenPoint(ptStart+normalCurr); path->AddWidenPoint(ptStart+normalNext); } } } /**************************************************************************\ * * Function Description: * * Performs a bevel join * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID BevelJoin( const GpVector2D &normalCurr, const GpVector2D &normalNext, const GpPointF &ptStart, const REAL limit, GpPath *path ) { // Outside Bevel Join. Simply join the end points of the two input normals. path->AddWidenPoint(ptStart + normalCurr); path->AddWidenPoint(ptStart + normalNext); } /**************************************************************************\ * * Function Description: * * Performs a miter join. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID MiterJoin( const GpVector2D &normalCurr, const GpVector2D &normalNext, const GpPointF &ptStart, const REAL limit, GpPath *path ) { GpVector2D gradCurr; // current gradient reversed. gradCurr.X = normalCurr.Y; gradCurr.Y = -normalCurr.X; GpVector2D gradNext; // next gradient. gradNext.X = -normalNext.Y; gradNext.Y = normalNext.X; REAL t1, t2; // temporary variables. GpPointF ptJoin; // If there is an intersection point and that intersection point is // closer to ptStart than the miter limit, then add the miter join // point. Otherwise revert to a bevel join. if( IntersectLines( ptStart + normalCurr, ptStart + normalCurr + gradCurr, ptStart + normalNext, ptStart + normalNext + gradNext, &t1, &t2, &ptJoin ) && // this won't get evaluated if IntersectLines fails. (distance_squared(ptStart, ptJoin) <= (limit*limit)) ) { path->AddWidenPoint(ptJoin); } else { BevelJoin(normalCurr, normalNext, ptStart, limit, path); } } /**************************************************************************\ * * Function Description: * * Performs a miter join. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID RoundJoin( const GpVector2D &normalCurr, const GpVector2D &normalNext, const GpPointF &ptStart, const REAL limit, GpPath *path ) { // !!! [asecchia] this is a really awful way of adding a round join. // we should change this to compute the control points directly or // even better - add a useful 'CurveTo' method on the path. REAL radius = const_cast(normalCurr).Norm(); GpRectF rect( ptStart.X-radius, ptStart.Y-radius, 2.0f*radius, 2.0f*radius ); REAL startAngle = (REAL)atan2(normalCurr.Y, normalCurr.X); if(startAngle < 0.0f) { startAngle += (REAL)(2.0*M_PI); } REAL sweepAngle = (REAL)atan2(normalNext.Y, normalNext.X); if(sweepAngle < 0.0f) { sweepAngle += (REAL)(2.0*M_PI); } sweepAngle -= startAngle; if(sweepAngle > (REAL)M_PI) { sweepAngle -= (REAL)(2.0*M_PI); } if(sweepAngle < (REAL)-M_PI) { sweepAngle += (REAL)(2.0*M_PI); } // Why doesn't this thing use radians?? startAngle = startAngle*180.0f/(REAL)M_PI; sweepAngle = sweepAngle*180.0f/(REAL)M_PI; // This is a really, really inconvenient AddArc interface! path->AddArc(rect, startAngle, sweepAngle); } /**************************************************************************\ * * Description: * * Function Pointer to a Join function. * * History: * * 10/22/2000 asecchia * Created. * \**************************************************************************/ typedef VOID (*CapProc)( const GpPointF &, const GpPointF &, GpPath * ); /**************************************************************************\ * * Function Description: * * Performs a Flat Cap * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID FlatCap( const GpPointF &ptLeft, const GpPointF &ptRight, GpPath *path ) { // Cap the open segment. path->AddWidenPoint(ptLeft); path->AddWidenPoint(ptRight); } /**************************************************************************\ * * Function Description: * * Performs a Flat Cap * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID TriangleCap( const GpPointF &ptLeft, const GpPointF &ptRight, GpPath *path ) { // Compute the midpoint of ptLeft and ptRight. GpPointF center( (ptLeft.X+ptRight.X)*0.5f, (ptLeft.Y+ptRight.Y)*0.5f ); // Cap the open segment. path->AddWidenPoint(ptLeft); GpVector2D V = ptRight-ptLeft; V *= 0.5; REAL tmp = V.X; V.X = V.Y; V.Y = -tmp; path->AddWidenPoint(V+center); path->AddWidenPoint(ptRight); } /**************************************************************************\ * * Function Description: * * Performs a Round Cap * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID RoundCap( const GpPointF &ptLeft, const GpPointF &ptRight, GpPath *path ) { // Compute the midpoint of ptLeft and ptRight. GpPointF center( (ptLeft.X+ptRight.X)*0.5f, (ptLeft.Y+ptRight.Y)*0.5f ); // Vector from left to right. We scale by 0.5 to optimize the rotation // code below. GpVector2D V = ptRight-ptLeft; V *= 0.5f; // 2 Bezier segments for a half circle with radius 1. static const GpPointF capPoints[7] = { GpPointF(-1.0f, 0.0f), GpPointF(-1.0f, -U_CIR), GpPointF(-U_CIR, -1.0f), GpPointF(0.0f, -1.0f), GpPointF(U_CIR, -1.0f), GpPointF(1.0f, -U_CIR), GpPointF(1.0f, 0.0f) }; GpPointF points[7]; // Rotate, scale, and translate the original half circle to the actual // end points we passed in. for(INT i = 0; i < 7; i++) { points[i].X = capPoints[i].X*V.X-capPoints[i].Y*V.Y+center.X; points[i].Y = capPoints[i].X*V.Y+capPoints[i].Y*V.X+center.Y; } // !!! the performance of this routine sux. We should be able to add // the points into the path directly. path->AddBeziers(points, 7); } /**************************************************************************\ * * Function Description: * * Performs a Double Round 'B'-shaped Cap * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ VOID DoubleRoundCap( const GpPointF &ptLeft, const GpPointF &ptRight, GpPath *path ) { // Compute the midpoint of ptLeft and ptRight. GpPointF center( (ptLeft.X+ptRight.X)*0.5f, (ptLeft.Y+ptRight.Y)*0.5f ); RoundCap(ptLeft, center, path); RoundCap(center, ptRight, path); } /**************************************************************************\ * * Function Description: * * Performs a Double Triangle Cap * * History: * * 10/22/2000 asecchia * Created. * \**************************************************************************/ VOID DoubleTriangleCap( const GpPointF &ptLeft, const GpPointF &ptRight, GpPath *path ) { // Compute the midpoint of ptLeft and ptRight. GpPointF center( (ptLeft.X+ptRight.X)*0.5f, (ptLeft.Y+ptRight.Y)*0.5f ); TriangleCap(ptLeft, center, path); TriangleCap(center, ptRight, path); } /**************************************************************************\ * * Function Description: * * Return a CapProc function for the given lineCap and DoubleCaps * * History: * * 10/23/2000 asecchia * Created. * \**************************************************************************/ CapProc GetCapProc(GpLineCap lineCap, BOOL DoubleCaps) { switch(lineCap) { case LineCapFlat: return FlatCap; case LineCapRound: if(DoubleCaps) { return DoubleRoundCap; } else { return RoundCap; } case LineCapTriangle: if(DoubleCaps) { return DoubleTriangleCap; } else { return TriangleCap; } default: // Invalid cap type for the widener. Use Flat. This will happen for // Anchor caps and Custom caps which are handled at a higher level. // See GpEndCapCreator. return FlatCap; }; } /**************************************************************************\ * * Function Description: * * Takes a (usually empty) path and widens the current spine path into it. * * History: * * 10/20/2000 asecchia * Created. * \**************************************************************************/ GpStatus GpPathWidener::Widen(GpPath *path) { // Can't widen the current path into itself for performance reasons. // We'd have to query the path points array every time we added a point. ASSERT(Path != path); // Normal array DynArray normalArray; DynArray spinePoints; // Initialize the subpath information. DynArray *subpathInfo; Path->GetSubpathInformation(&subpathInfo); // Initialize the pointers to the original path data. const GpPointF *originalPoints = Path->GetPathPoints(); const BYTE *originalTypes = Path->GetPathTypes(); // Initialize the Join function. JoinProc join; switch(Pen->Join) { case LineJoinMiter: join = MiterJoin; break; case LineJoinBevel: join = BevelJoin; break; case LineJoinRound: join = RoundJoin; break; default: // Invalid join type. Use Bevel, but fire an ASSERT to make developers // fix this code if they added a Join type. ONCE(WARNING(("Invalid Join type selected. Defaulting to Bevel"))); join = BevelJoin; }; // Initialize the various cap functions. CapProc startCap = GetCapProc(Pen->StartCap, DoubleCaps); CapProc endCap = GetCapProc(Pen->EndCap, DoubleCaps); if(Pen->StartCap == LineCapCustom) { ASSERT(Pen->CustomStartCap); GpLineCap tempCap; Pen->CustomStartCap->GetBaseCap(&tempCap); startCap = GetCapProc(tempCap, DoubleCaps); } if(Pen->EndCap == LineCapCustom) { ASSERT(Pen->CustomEndCap); GpLineCap tempCap; Pen->CustomEndCap->GetBaseCap(&tempCap); endCap = GetCapProc(tempCap, DoubleCaps); } CapProc dashCap = GetCapProc(Pen->DashCap, DoubleCaps); // Initialize the compound line data. ASSERT(Pen->CompoundCount >= 0); INT compoundCount = Pen->CompoundCount; if(compoundCount==0) { compoundCount = 2; } REAL penAlignmentOffset; switch(Pen->PenAlignment) { case PenAlignmentLeft: penAlignmentOffset = StrokeWidth/2.0f; break; case PenAlignmentRight: penAlignmentOffset = -StrokeWidth/2.0f; break; default: // Center pen only - handle inset pen at a much higher level. ASSERT(Pen->PenAlignment == PenAlignmentCenter); penAlignmentOffset=0.0f; }; // Done initialization, start processing each subpath. for(INT currentSubpath = 0; currentSubpath < subpathInfo->GetCount(); currentSubpath++) { // Figure out the subpath record and Normal information. GpPath::SubpathInfo subpath = (*subpathInfo)[currentSubpath]; // Remove all points that are the same from the subpath. // This initializes the spinePoints array. if( ComputeNonDegeneratePoints( &spinePoints, subpath, originalPoints) != Ok) { return OutOfMemory; } // skip this subpath if there aren't enough points left. if(spinePoints.GetCount() < 2) { continue; } // get a convenient pointer to the main spine points - after removing // degenerates. Const because we're not going to modify the points. const GpPointF *spine = spinePoints.GetDataBuffer(); // Calculate the normals to all the points in the spine array. // The normals are normal to the edge between two points, so in an open // line segment, there is one less normal than there are points in the // spine. We handle this by initializing the first normal to (0,0). // The normals are all unit vectors and need to be scaled. ComputeSubpathNormals( &normalArray, spinePoints.GetCount(), subpath.IsClosed, spine ); GpVector2D *normals = normalArray.GetDataBuffer(); // Loop over all the compound line segments. // If there is no compound line we have set the compoundCount to 2. for(INT compoundIndex=0; compoundIndex < compoundCount/2; compoundIndex++) { // Compute the left and right offset. REAL left; REAL right; if(Pen->CompoundCount != 0) { // This is a compound line. ASSERT(Pen->CompoundArray != NULL); left = (0.5f-Pen->CompoundArray[compoundIndex*2]) * StrokeWidth; right = (0.5f-Pen->CompoundArray[compoundIndex*2+1]) * StrokeWidth; } else { // standard non-compound line. left = StrokeWidth/2.0f; right = -left; } left += penAlignmentOffset; right += penAlignmentOffset; INT startIdx = 0; INT endIdx = spinePoints.GetCount()-1; if(!subpath.IsClosed) { // Adjust the count for the join loop to represent the fact that // we won't join the beginning and end of an open line segment. // Note Open line segments skip the first point handling it // in the final cap. startIdx++; endIdx--; } // // Widen to the left. // // walk the spine forwards joining each point and emitting the // appropriate set of points for the join. INT ptIndex; for(ptIndex = startIdx; ptIndex <= endIdx; ptIndex++) { // Modulo arithmetic for the next point. INT ptIndexNext = ptIndex+1; if(ptIndexNext == spinePoints.GetCount()) { ptIndexNext = 0; } GpVector2D normalCurr = normals[ptIndex]*left; GpVector2D normalNext = normals[ptIndexNext]*left; // Check to see if it's an inside or outside join. If the // determinant of the two normals is negative, it's an outside // join - i.e. space between the line segment endpoints that need to // be joined. If it's positive, it's an inside join and the two // line segments overlap. REAL det = Determinant(normalCurr, normalNext); if(REALABS(det) < REAL_EPSILON) { // This is the case where the angle of curvature for this // join is so small, we don't care which end point we pick. // REAL_EPSILON is probably too small for this - should // be about 0.25 of a device pixel or something. path->AddWidenPoint(spine[ptIndex] + normalCurr); } else { // We need to do some work to join the segments. if(det > 0) { // Outside Joins. join( normalCurr, normalNext, spine[ptIndex], Pen->MiterLimit*StrokeWidth, path ); } else { INT ptIndexPrev = ptIndex-1; if(ptIndexPrev == -1) { ptIndexPrev = spinePoints.GetCount()-1; } InsideJoin( normalCurr, normalNext, spine[ptIndex], spine[ptIndexPrev], spine[ptIndexNext], path ); } } } // Handle the final point in the spine. if(subpath.IsClosed) { // Make a closed subpath out of the left widened points. path->CloseFigure(); } else { // Cap the open segment. CapProc cap = endCap; if( IsDashType( originalTypes[subpath.StartIndex+subpath.Count-1] )) { // actually it's a dash cap, not an end cap. cap = dashCap; } cap( spine[spinePoints.GetCount()-1] + (normals[spinePoints.GetCount()-1]*left), spine[spinePoints.GetCount()-1] + (normals[spinePoints.GetCount()-1]*right), path ); } // // Widen to the right. // // walk the spine backwards joining each point and emitting the // appropriate set of points for the join. for(ptIndex = endIdx; ptIndex >= startIdx; ptIndex--) { // Modulo arithmetic for the next point. INT ptIndexNext = ptIndex+1; if(ptIndexNext == spinePoints.GetCount()) { ptIndexNext = 0; } GpVector2D normalCurr = normals[ptIndex]*right; GpVector2D normalNext = normals[ptIndexNext]*right; // Check to see if it's an inside or outside join. If the // determinant of the two normals is negative, it's an outside // join - i.e. space between the line segment endpoints that need to // be joined. If it's positive, it's an inside join and the two // line segments overlap. REAL det = Determinant(normalNext, normalCurr); if(REALABS(det) < REAL_EPSILON) { // This is the case where the angle of curvature for this // join is so small, we don't care which end point we pick. // REAL_EPSILON is probably too small for this - should // be about 0.25 of a device pixel or something. path->AddWidenPoint(spine[ptIndex] + normalNext); } else { // We need to do some work to join the segments. if(det > 0) { // Outside Joins. join( normalNext, // note the order is flipped for normalCurr, // the backward traversal. spine[ptIndex], Pen->MiterLimit*StrokeWidth, path ); } else { INT ptIndexPrev = ptIndex-1; if(ptIndexPrev == -1) { ptIndexPrev = spinePoints.GetCount()-1; } InsideJoin( normalNext, // note the order is flipped for normalCurr, // the backward traversal. spine[ptIndex], spine[ptIndexNext], spine[ptIndexPrev], path ); } } } // Handle the first point in the spine. if(!subpath.IsClosed) { // Cap the open segment. CapProc cap = startCap; if(IsDashType(originalTypes[subpath.StartIndex])) { // actually it's a dash cap, not a start cap. cap = dashCap; } cap( spine[0] + (normals[1]*right), spine[0] + (normals[1]*left), path ); } // Close the previous contour. For open segments this will close // off the widening with the end cap for the first point. For // closed paths, the left widened points have already been closed // off, so close off the right widened points. path->CloseFigure(); } } return Ok; }