/// <summary> /// Helper routine to render two distinct stroke nodes /// </summary> private static void RenderTwoStrokeNodes( StreamGeometryContext context, StrokeNode strokeNodePrevious, Rect strokeNodePreviousBounds, StrokeNode strokeNodeCurrent, Rect strokeNodeCurrentBounds, List<Point> pointBuffer1, List<Point> pointBuffer2, List<Point> pointBuffer3 #if DEBUG_RENDERING_FEEDBACK ,DrawingContext debugDC, double feedbackSize, bool showFeedback #endif ) { Debug.Assert(pointBuffer1 != null); Debug.Assert(pointBuffer2 != null); Debug.Assert(pointBuffer3 != null); Debug.Assert(context != null); //see if we need to render a quad - if there is not at least a 70% overlap if (FuzzyContains(strokeNodePreviousBounds, strokeNodeCurrentBounds, 70d) != RectCompareResult.NoItersection) { //we're between 100% and 70% overlapped //just render two distinct figures with a connecting quad (if needed) strokeNodePrevious.GetContourPoints(pointBuffer1); AddFigureToStreamGeometryContext(context, pointBuffer1, strokeNodePrevious.IsEllipse/*isBezierFigure*/); Quad quad = strokeNodeCurrent.GetConnectingQuad(); if (!quad.IsEmpty) { pointBuffer3.Add(quad.A); pointBuffer3.Add(quad.B); pointBuffer3.Add(quad.C); pointBuffer3.Add(quad.D); AddFigureToStreamGeometryContext(context, pointBuffer3, false/*isBezierFigure*/); } strokeNodeCurrent.GetContourPoints(pointBuffer2); AddFigureToStreamGeometryContext(context, pointBuffer2, strokeNodeCurrent.IsEllipse/*isBezierFigure*/); } else { //we're less than 70% overlapped, it's safe to run our optimization #if DEBUG_RENDERING_FEEDBACK strokeNodeCurrent.GetPointsAtStartOfSegment(pointBuffer1, pointBuffer2, debugDC, feedbackSize, showFeedback); strokeNodeCurrent.GetPointsAtEndOfSegment(pointBuffer1, pointBuffer2, debugDC, feedbackSize, showFeedback); #else strokeNodeCurrent.GetPointsAtStartOfSegment(pointBuffer1, pointBuffer2); strokeNodeCurrent.GetPointsAtEndOfSegment(pointBuffer1, pointBuffer2); #endif //render ReverseDCPointsRenderAndClear(context, pointBuffer1, pointBuffer2, pointBuffer3, strokeNodeCurrent.IsEllipse, false/*clear the point collections*/); } }
internal static void CalcGeometryAndBounds(StrokeNodeIterator iterator, DrawingAttributes drawingAttributes, #if DEBUG_RENDERING_FEEDBACK DrawingContext debugDC, double feedbackSize, bool showFeedback, #endif bool calculateBounds, out Geometry geometry, out Rect bounds) { Debug.Assert(iterator != null && drawingAttributes != null); //we can use our new algorithm for identity only. Matrix stylusTipTransform = drawingAttributes.StylusTipTransform; if (stylusTipTransform != Matrix.Identity && stylusTipTransform._type != MatrixTypes.TRANSFORM_IS_SCALING) { //second best optimization CalcGeometryAndBoundsWithTransform(iterator, drawingAttributes, stylusTipTransform._type, calculateBounds, out geometry, out bounds); } else { StreamGeometry streamGeometry = new StreamGeometry(); streamGeometry.FillRule = FillRule.Nonzero; StreamGeometryContext context = streamGeometry.Open(); geometry = streamGeometry; Rect empty = Rect.Empty; bounds = empty; try { // // We keep track of three StrokeNodes as we iterate across // the Stroke. Since these are structs, the default ctor will // be called and .IsValid will be false until we initialize them // StrokeNode emptyStrokeNode = new StrokeNode(); StrokeNode prevPrevStrokeNode = new StrokeNode(); StrokeNode prevStrokeNode = new StrokeNode(); StrokeNode strokeNode = new StrokeNode(); Rect prevPrevStrokeNodeBounds = empty; Rect prevStrokeNodeBounds = empty; Rect strokeNodeBounds = empty; //percentIntersect is a function of drawingAttributes height / width double percentIntersect = 95d; double maxExtent = Math.Max(drawingAttributes.Height, drawingAttributes.Width); percentIntersect += Math.Min(4.99999d, ((maxExtent / 20d) * 5d)); double prevAngle = double.MinValue; bool isStartOfSegment = true; bool isEllipse = drawingAttributes.StylusTip == StylusTip.Ellipse; bool ignorePressure = drawingAttributes.IgnorePressure; // // Two List<Point>'s that get reused for adding figures // to the streamgeometry. // List<Point> pathFigureABSide = new List<Point>();//don't prealloc. It causes Gen2 collections to rise and doesn't help execution time List<Point> pathFigureDCSide = new List<Point>(); List<Point> polyLinePoints = new List<Point>(4); int iteratorCount = iterator.Count; for (int index = 0, previousIndex = -1; index < iteratorCount; ) { if (!prevPrevStrokeNode.IsValid) { if (prevStrokeNode.IsValid) { //we're sliding our pointers forward prevPrevStrokeNode = prevStrokeNode; prevPrevStrokeNodeBounds = prevStrokeNodeBounds; prevStrokeNode = emptyStrokeNode; } else { prevPrevStrokeNode = iterator[index++, previousIndex++]; prevPrevStrokeNodeBounds = prevPrevStrokeNode.GetBounds(); continue; //so we always check if index < iterator.Count } } //we know prevPrevStrokeNode is valid if (!prevStrokeNode.IsValid) { if (strokeNode.IsValid) { //we're sliding our pointers forward prevStrokeNode = strokeNode; prevStrokeNodeBounds = strokeNodeBounds; strokeNode = emptyStrokeNode; } else { //get the next strokeNode, but don't automatically update previousIndex prevStrokeNode = iterator[index++, previousIndex]; prevStrokeNodeBounds = prevStrokeNode.GetBounds(); RectCompareResult result = FuzzyContains( prevStrokeNodeBounds, prevPrevStrokeNodeBounds, isStartOfSegment ? 99.99999d : percentIntersect); if (result == RectCompareResult.Rect1ContainsRect2) { // this node already contains the prevPrevStrokeNodeBounds (PP): // // |------------| // | |----| | // | | PP | P | // | |----| | // |------------| // prevPrevStrokeNode = iterator[index - 1, prevPrevStrokeNode.Index - 1]; ; prevPrevStrokeNodeBounds = Rect.Union(prevStrokeNodeBounds, prevPrevStrokeNodeBounds); // at this point prevPrevStrokeNodeBounds already contains this node // we can just ignore this node prevStrokeNode = emptyStrokeNode; // update previousIndex to point to this node previousIndex = index - 1; // go back to our main loop continue; } else if (result == RectCompareResult.Rect2ContainsRect1) { // this prevPrevStrokeNodeBounds (PP) already contains this node: // // |------------| // | |----|| // | PP | P || // | |----|| // |------------| // //prevPrevStrokeNodeBounds already contains this node //we can just ignore this node prevStrokeNode = emptyStrokeNode; // go back to our main loop, but do not update previousIndex // because it should continue to point to previousPrevious continue; } Debug.Assert(!prevStrokeNode.GetConnectingQuad().IsEmpty, "prevStrokeNode.GetConnectingQuad() is Empty!"); // if neither was true, we now have two of our three nodes required to // start our computation, we need to update previousIndex to point // to our current, valid prevStrokeNode previousIndex = index - 1; continue; //so we always check if index < iterator.Count } } //we know prevPrevStrokeNode and prevStrokeNode are both valid if (!strokeNode.IsValid) { strokeNode = iterator[index++, previousIndex]; strokeNodeBounds = strokeNode.GetBounds(); RectCompareResult result = FuzzyContains( strokeNodeBounds, prevStrokeNodeBounds, isStartOfSegment ? 99.99999 : percentIntersect); RectCompareResult result2 = FuzzyContains( strokeNodeBounds, prevPrevStrokeNodeBounds, isStartOfSegment ? 99.99999 : percentIntersect); if ( isStartOfSegment && result == RectCompareResult.Rect1ContainsRect2 && result2 == RectCompareResult.Rect1ContainsRect2) { if (pathFigureABSide.Count > 0) { //we've started a stroke, we need to end it before resetting //prevPrev #if DEBUG_RENDERING_FEEDBACK prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide); #endif //render ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/); } //we're resetting //prevPrevStrokeNode. We need to gen one //without a connecting quad prevPrevStrokeNode = iterator[index - 1, prevPrevStrokeNode.Index - 1]; prevPrevStrokeNodeBounds = prevPrevStrokeNode.GetBounds(); prevStrokeNode = emptyStrokeNode; strokeNode = emptyStrokeNode; // increment previousIndex to to point to this node previousIndex = index - 1; continue; } else if (result == RectCompareResult.Rect1ContainsRect2) { // this node (C) already contains the prevStrokeNodeBounds (P): // // |------------| // |----| | |----| | // | PP | | | P | C | // |----| | |----| | // |------------| // //we have to generate a new stroke node that points //to pp since the connecting quad from C to P could be empty //if they have the same point strokeNode = iterator[index - 1, prevStrokeNode.Index - 1]; if (!strokeNode.GetConnectingQuad().IsEmpty) { //only update prevStrokeNode if we have a valid connecting quad prevStrokeNode = strokeNode; prevStrokeNodeBounds = Rect.Union(strokeNodeBounds, prevStrokeNodeBounds); // update previousIndex, since it should point to this node now previousIndex = index - 1; } // at this point we can just ignore this node strokeNode = emptyStrokeNode; //strokeNodeBounds = empty; prevAngle = double.MinValue; //invalidate // go back to our main loop continue; } else if (result == RectCompareResult.Rect2ContainsRect1) { // this prevStrokeNodeBounds (P) already contains this node (C): // // |------------| // |----| | |----|| // | PP | | P | C || // |----| | |----|| // |------------| // //prevStrokeNodeBounds already contains this node //we can just ignore this node strokeNode = emptyStrokeNode; // go back to our main loop, but do not update previousIndex // because it should continue to point to previous continue; } Debug.Assert(!strokeNode.GetConnectingQuad().IsEmpty, "strokeNode.GetConnectingQuad was empty, this is unexpected"); // // NOTE: we do not check if C contains PP, or PP contains C because // that indicates a change in direction, which we handle below // // if neither was true P and C are separate, // we now have all three nodes required to // start our computation, we need to update previousIndex to point // to our current, valid prevStrokeNode previousIndex = index - 1; } // see if we have an overlap between the first and third node bool overlap = prevPrevStrokeNodeBounds.IntersectsWith(strokeNodeBounds); // prevPrevStrokeNode, prevStrokeNode and strokeNode are all // valid nodes now. Now we need to figure out what do add to our // PathFigure. First calc bounds on the strokeNode we know we need to render if (calculateBounds) { bounds.Union(prevStrokeNodeBounds); } // determine what points to add to pathFigureABSide and pathFigureDCSide // from prevPrevStrokeNode if (pathFigureABSide.Count == 0) { Debug.Assert(pathFigureDCSide.Count == 0); if (calculateBounds) { bounds.Union(prevPrevStrokeNodeBounds); } if (isStartOfSegment && overlap) { //render a complete first stroke node or we can get artifacts prevPrevStrokeNode.GetContourPoints(polyLinePoints); AddFigureToStreamGeometryContext(context, polyLinePoints, prevPrevStrokeNode.IsEllipse/*isBezierFigure*/); polyLinePoints.Clear(); } // we're starting a new pathfigure // we need to add parts of the prevPrevStrokeNode contour // to pathFigureABSide and pathFigureDCSide #if DEBUG_RENDERING_FEEDBACK prevStrokeNode.GetPointsAtStartOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else prevStrokeNode.GetPointsAtStartOfSegment(pathFigureABSide, pathFigureDCSide); #endif //set our marker, we're no longer at the start of the stroke isStartOfSegment = false; } if (prevAngle == double.MinValue) { //prevAngle is no longer valid prevAngle = GetAngleBetween(prevPrevStrokeNode.Position, prevStrokeNode.Position); } double delta = GetAngleDeltaFromLast(prevStrokeNode.Position, strokeNode.Position, ref prevAngle); bool directionChangedOverAbsoluteThreshold = Math.Abs(delta) > 90d && Math.Abs(delta) < (360d - 90d); bool directionChangedOverOverlapThreshold = overlap && !(ignorePressure || strokeNode.PressureFactor == 1f) && Math.Abs(delta) > 30d && Math.Abs(delta) < (360d - 30d); double prevArea = prevStrokeNodeBounds.Height * prevStrokeNodeBounds.Width; double currArea = strokeNodeBounds.Height * strokeNodeBounds.Width; bool areaChanged = !(prevArea == currArea && prevArea == (prevPrevStrokeNodeBounds.Height * prevPrevStrokeNodeBounds.Width)); bool areaChangeOverThreshold = false; if (overlap && areaChanged) { if ((Math.Min(prevArea, currArea) / Math.Max(prevArea, currArea)) <= 0.90d) { //the min area is < 70% of the max area areaChangeOverThreshold = true; } } if (areaChanged || delta != 0.0d || index >= iteratorCount) { //the area changed between the three nodes OR there was an angle delta OR we're at the end //of the stroke... either way, this is a significant node. If not, we're going to drop it. if ((overlap && (directionChangedOverOverlapThreshold || areaChangeOverThreshold)) || directionChangedOverAbsoluteThreshold) { // // we need to stop the pathfigure at P // and render the pathfigure // // |--| |--| |--||--| |------| // |PP|------|P | |PP||P | |PP P C| // |--| |--| |--||--| |------| // / |C | // |--| |--| // |C | // |--| #if DEBUG_RENDERING_FEEDBACK prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else //end the figure prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide); #endif //render ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/); if (areaChangeOverThreshold) { //render a complete stroke node or we can get artifacts prevStrokeNode.GetContourPoints(polyLinePoints); AddFigureToStreamGeometryContext(context, polyLinePoints, prevStrokeNode.IsEllipse/*isBezierFigure*/); polyLinePoints.Clear(); } } else { // // direction didn't change over the threshold, add the midpoint data // |--| |--| // |PP|------|P | // |--| |--| // \ // |--| // |C | // |--| bool endSegment; //flag that tell us if we missed an intersection #if DEBUG_RENDERING_FEEDBACK strokeNode.GetPointsAtMiddleSegment(prevStrokeNode, delta, pathFigureABSide, pathFigureDCSide, out endSegment, debugDC, feedbackSize, showFeedback); #else strokeNode.GetPointsAtMiddleSegment(prevStrokeNode, delta, pathFigureABSide, pathFigureDCSide, out endSegment); #endif if (endSegment) { //we have a missing intersection, we need to end the //segment at P #if DEBUG_RENDERING_FEEDBACK prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else //end the figure prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide); #endif //render ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/); } } } // // either way... slide our pointers forward, to do this, we simply mark // our first pointer as 'empty' // prevPrevStrokeNode = emptyStrokeNode; prevPrevStrokeNodeBounds = empty; } // // anything left to render? // if (prevPrevStrokeNode.IsValid) { if (prevStrokeNode.IsValid) { if (calculateBounds) { bounds.Union(prevPrevStrokeNodeBounds); bounds.Union(prevStrokeNodeBounds); } Debug.Assert(!strokeNode.IsValid); // // we never made it to strokeNode, render two points, OR // strokeNode was a dupe // if (pathFigureABSide.Count > 0) { #if DEBUG_RENDERING_FEEDBACK prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else // // strokeNode was a dupe, we just need to render the end of the stroke // which is at prevStrokeNode // prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide); #endif //render ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, false/*clear the point collections*/); } else { // we've only seen two points to render Debug.Assert(pathFigureDCSide.Count == 0); //contains all the logic to render two stroke nodes RenderTwoStrokeNodes( context, prevPrevStrokeNode, prevPrevStrokeNodeBounds, prevStrokeNode, prevStrokeNodeBounds, pathFigureABSide, pathFigureDCSide, polyLinePoints #if DEBUG_RENDERING_FEEDBACK ,debugDC, feedbackSize, showFeedback #endif ); } } else { if (calculateBounds) { bounds.Union(prevPrevStrokeNodeBounds); } // we only have a single point to render Debug.Assert(pathFigureABSide.Count == 0); prevPrevStrokeNode.GetContourPoints(pathFigureABSide); AddFigureToStreamGeometryContext(context, pathFigureABSide, prevPrevStrokeNode.IsEllipse/*isBezierFigure*/); } } else if (prevStrokeNode.IsValid && strokeNode.IsValid) { if (calculateBounds) { bounds.Union(prevStrokeNodeBounds); bounds.Union(strokeNodeBounds); } // typical case, we hit the end of the stroke // see if we need to start a stroke, or just end one if (pathFigureABSide.Count > 0) { #if DEBUG_RENDERING_FEEDBACK strokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback); #else strokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide); #endif //render ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, false/*clear the point collections*/); if (FuzzyContains(strokeNodeBounds, prevStrokeNodeBounds, 70d) != RectCompareResult.NoItersection) { //render a complete stroke node or we can get artifacts strokeNode.GetContourPoints(polyLinePoints); AddFigureToStreamGeometryContext(context, polyLinePoints, strokeNode.IsEllipse/*isBezierFigure*/); } } else { Debug.Assert(pathFigureDCSide.Count == 0); //contains all the logic to render two stroke nodes RenderTwoStrokeNodes( context, prevStrokeNode, prevStrokeNodeBounds, strokeNode, strokeNodeBounds, pathFigureABSide, pathFigureDCSide, polyLinePoints #if DEBUG_RENDERING_FEEDBACK ,debugDC, feedbackSize, showFeedback #endif ); } } } finally { context.Close(); geometry.Freeze(); } } }